AVM Example for Spec Kit

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

Language Coverage

This guide currently covers Bicep implementations only. The Terraform version is actively being developed and will be added soon. Stay tuned for updates!

Prerequisites

You will need the following tools and components to complete this guide:

Coming soon!

Before you begin, make sure you have these tools installed in your development environment!

Solution Architecture

Before we begin coding, it is important to have details about what the infrastructure architecture will include. For our example, using AVM modules, we will be building a solution that will host a legacy business application running as a single Windows Server 2016 virtual machine (VM) with at least 2 CPU cores, 8GB RAM, Standard HDD OS disk, and a 500GB data disk.

The VM is accessible via Azure Bastion using secure RDP access (no public IP exposure). The solution needs an Azure Storage Account with an HDD-backed file share connected via private endpoint, and an Azure Key Vault to securely store the VM administrator password generated at deployment time. The VM must not be accessible from the internet, and all diagnostic logs will be captured in a Log Analytics workspace with critical alerts configured for VM availability, disk utilization, and Key Vault access failures.

Azure VM Solution Architecture

Bootstrapping

Tip

On a Windows PC, to get the uv package manager CLI tool required for locally installing the Specify CLI, run the following command:

winget install astral-sh.uv
  1. To install Spec Kit locally, run the following command in an elevated terminal:

    uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
  2. Create a new directory for your Spec Kit project and navigate into it - this folder ideally already exists as a git repository:

    mkdir avm-workload
    cd avm-workload
  3. Set your default branch to main (initializing Spec Kit will configure this working folder as a git repository if it isn’t one already):

    git config --global init.defaultBranch main
  4. Initialize a new Spec Kit project:

    specify init .

    If the folder has already been set up as a repository, the specify tool will warn you that the folder is not empty. Just confirm that you want to proceed.

    As we haven’t defined the AI assistant in our init command, specify will prompt us to choose one. Select copilot (GitHub Copilot) from the list. Similarly, as we haven’t defined the script type, specify will prompt us to choose one. Select ps (PowerShell) from the list.

    Alternatively, you can provide these parameters directly in the init command, like so:

    specify init . --ai copilot --script ps
    βž• Expand to see the results

    Note: As Spec Kit evolves, the output may change over time. This example is meant to give you an idea of what the user interface looks like.

    Click through the tabs to see the details!

    You should see something like this:
    Specify Bootstrap

    In your project folder, you should now see the following files and folders created by the specify tool:

    <!-- markdownlint-disable -->
    avm-workload
    β”‚
    β”œβ”€β”€β”€.github
    β”‚   β”œβ”€β”€β”€agents
    β”‚   β”‚       speckit.analyze.agent.md
    β”‚   β”‚       speckit.checklist.agent.md
    β”‚   β”‚       speckit.clarify.agent.md
    β”‚   β”‚       speckit.constitution.agent.md
    β”‚   β”‚       speckit.implement.agent.md
    β”‚   β”‚       speckit.plan.agent.md
    β”‚   β”‚       speckit.specify.agent.md
    β”‚   β”‚       speckit.tasks.agent.md
    β”‚   β”‚       speckit.taskstoissues.agent.md
    β”‚   β”‚
    β”‚   └───prompts
    β”‚           speckit.analyze.prompt.md
    β”‚           speckit.checklist.prompt.md
    β”‚           speckit.clarify.prompt.md
    β”‚           speckit.constitution.prompt.md
    β”‚           speckit.implement.prompt.md
    β”‚           speckit.plan.prompt.md
    β”‚           speckit.specify.prompt.md
    β”‚           speckit.tasks.prompt.md
    β”‚           speckit.taskstoissues.prompt.md
    β”‚
    β”œβ”€β”€β”€.specify
    β”‚   β”œβ”€β”€β”€memory
    β”‚   β”‚       constitution.md
    β”‚   β”‚
    β”‚   β”œβ”€β”€β”€scripts
    β”‚   β”‚   └───powershell
    β”‚   β”‚           check-prerequisites.ps1
    β”‚   β”‚           common.ps1
    β”‚   β”‚           create-new-feature.ps1
    β”‚   β”‚           setup-plan.ps1
    β”‚   β”‚           update-agent-context.ps1
    β”‚   β”‚
    β”‚   └───templates
    β”‚           agent-file-template.md
    β”‚           checklist-template.md
    β”‚           plan-template.md
    β”‚           spec-template.md
    β”‚           tasks-template.md
    β”‚
    β”œβ”€β”€β”€.vscode
    β”‚       settings.json
    β”‚
    └───infra
        β”œβ”€β”€β”€modules
        β”‚   β”œβ”€β”€β”€compute
        β”‚   β”œβ”€β”€β”€monitoring
        β”‚   β”œβ”€β”€β”€networking
        β”‚   β”œβ”€β”€β”€security
        β”‚   β”œβ”€β”€β”€shared
        β”‚   └───storage
        └───tests
            └───compliance
  5. Spec Kit automatically commits this step to the git repository with following comment: Initial commit from Specify template.

  6. The rest of the steps will be performed using GitHub Copilot Chat in VS Code: Start your VS Code environment, open or add the newly created folder to your workspace, and navigate to GitHub Copilot Chat using the dialog icon on the top of the window or by hitting CTRL+ALT+I.

Making it real

Spec Kit follows a structured workflow that guides you through each phase of solution development, from establishing foundational principles to implementing the final code. To learn more about Spec Kit, see the Spec Kit overview section.

flowchart LR
    A[1\. Constitution] --> B[2\. Specify]
    B --> C["3\. Clarify<br>(Optional)"]
    C --> D[4\. Plan]
    D --> E["5\. Checklist<br>(Optional)"]
    E --> F[6\. Tasks]
    F --> G["7\. Analyze<br>(Optional)"]
    G --> H[8\. Implement]

    click A "#1-constitution"
    click B "#2-specify"
    click C "#3-clarify-optional"
    click D "#4-plan"
    click E "#5checklist-optional"
    click F "#6-tasks"
    click G "#7-analyze-optional"
    click H "#8-implement"

    style C fill:#e1f5ff
    style E fill:#e1f5ff
    style G fill:#e1f5ff

To implement our example solution using AVM modules, we will walk through each of these steps in detail.

Each of the below steps will typically take 3-8 minutes to complete, depending on the complexity of your specification, the performance of the AI model you are using, and your reaction time to answer any outstanding questions and review and approve the generated content.

Choose your LLM

Changing the LLM does make a difference. We highly encourage you test different models to see which one works best for your needs.

Note: At the time of writing this article, we tested our prompts with Claude Sonnet 4.5.

Know before you go
  1. As Spec Kit uses a set of built-in and system tools and scripts, you will need to approve the execution of each of these steps. Make sure you understand the impact of these commands before approving and proceeding!
    Here’s an example:
Specify Approve Scripts
  1. In some cases, your account might exceed GitHub’s API rate limits when using GitHub Copilot with Spec Kit. If that happens, please wait for a while (usually an hour or so) and try again.

1. Constitution

Spec Kit uses /speckit.constitution to generate the constitution.md file. The constitution can be evolved through iterating over the constitution.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.constitution.

Info

To learn more about what the constitution should include, see the Constitution chapter in the Spec Kit article.

βž• Before running /speckit.constitution (Expand)

Notice what the constitution.md file looks like before running the related prompt. It is just a template with placeholders, defining the structure:

Note: As Spec Kit evolves, the content of this template may change over time. This example is meant to give you an idea of what the starting point looks like.

<!-- markdownlint-disable -->
# [PROJECT_NAME] Constitution
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->

## Core Principles

### [PRINCIPLE_1_NAME]
<!-- Example: I. Library-First -->
[PRINCIPLE_1_DESCRIPTION]
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->

### [PRINCIPLE_2_NAME]
<!-- Example: II. CLI Interface -->
[PRINCIPLE_2_DESCRIPTION]
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args β†’ stdout, errors β†’ stderr; Support JSON + human-readable formats -->

### [PRINCIPLE_3_NAME]
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
[PRINCIPLE_3_DESCRIPTION]
<!-- Example: TDD mandatory: Tests written β†’ User approved β†’ Tests fail β†’ Then implement; Red-Green-Refactor cycle strictly enforced -->

### [PRINCIPLE_4_NAME]
<!-- Example: IV. Integration Testing -->
[PRINCIPLE_4_DESCRIPTION]
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->

### [PRINCIPLE_5_NAME]
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
[PRINCIPLE_5_DESCRIPTION]
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->

## [SECTION_2_NAME]
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->

[SECTION_2_CONTENT]
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->

## [SECTION_3_NAME]
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->

[SECTION_3_CONTENT]
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->

## Governance
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->

[GOVERNANCE_RULES]
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->

**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
  1. Run the following prompt to generate the constitution for our example:
/speckit.constitution Fill the constitution with the typical requirements of a legacy Azure workload (needed to be retained for compliance reasons; no high-availability requirements; no disaster recovery requirements; no scalability requirements), defined as infrastructure-as-code, in Bicep language, built only with Azure Verified Modules (AVM). Always try to implement every feature with Bicep first (using Infra-as-code), and only use custom scripts when it's not possible otherwise. Follow IaC best practices: define everything in a single template, and let ARM manage dependencies and the order of deployment for each Azure resource.

Security and reliability best practices must be followed under all circumstances.

The naming convention is to use just enough random characters to make the name unique and have the Azure resource type reflected in the name. Resource type specific character and length limitations must be respected.

Before running a deployment, always run a validation.

Deploy everything to the US West 3 datacenter region.
βž• Expand to see the results

The most important artifact created in this phase is the constitution.md file. Based on your inputs, additional files may also be created or updated, such as spec-template.md , plan-template.md, and tasks-template.md. These would further improve the quality of the generated artifacts in later phases.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Constitution Chat Results

In the constitution.md file, notice, how changes implemented by Copilot are not final yet, until you review, and approve them by clicking on the “Keep” button. You can either do this on a paragraph-by-paragraph basis, or for the entire document at once by clicking on the “Keep” button in the main Copilot chat window.:

Constitution Before Approvals
<!-- markdownlint-disable -->
<!--
SYNC IMPACT REPORT
==================
Version Change: N/A β†’ 1.0.0 (Initial constitution ratification)

Modified Principles:
- N/A (Initial version)

Added Sections:
- Core Principles (6 principles defined)
- Infrastructure Standards
- Security & Compliance Requirements
- Development Workflow
- Governance

Removed Sections:
- N/A (Initial version)

Templates Requiring Updates:
- βœ… plan-template.md: Constitution Check section aligns with principles
- βœ… spec-template.md: Requirements structure aligns with security principles
- βœ… tasks-template.md: Task categorization aligns with validation-first workflow

Follow-up TODOs:
- None (all placeholders filled)

Change Rationale:
- MAJOR version (1.0.0) because this is the initial constitution establishing governance framework
- Principles focused on legacy workload characteristics: compliance retention, IaC-first with Bicep, AVM-only modules, validation-before-deployment
-->

# Legacy Azure Workload Constitution

## Core Principles

### I. Infrastructure-as-Code First (NON-NEGOTIABLE)
All Azure resources MUST be defined in Bicep templates. Manual Azure Portal configurations are STRICTLY PROHIBITED.

**Rationale**: Ensures compliance auditability, repeatability, and version control for regulatory requirements. Manual changes create configuration drift that violates compliance mandates.

**Rules**:
- Every feature starts with Bicep code defining the infrastructure
- Custom scripts are permitted ONLY when Bicep/ARM capabilities are insufficient
- All infrastructure changes MUST go through version control
- Single-template approach: define everything in one main.bicep, let ARM handle dependencies

### II. AVM-Only Modules
All Bicep infrastructure MUST use Azure Verified Modules (AVM). Direct resource declarations are permitted only when no AVM module exists.

**Rationale**: AVM modules are officially maintained, follow security best practices, and are compliance-tested by Microsoft.

**Rules**:
- Search for AVM module first (using `#list_avm_metadata` tool)
- Use latest stable version of AVM modules
- Document justification when direct resource declaration is necessary
- Follow AVM module documentation for parameter configuration

### III. Validation Before Deployment (NON-NEGOTIABLE)
Every deployment MUST be preceded by ARM validation. Deployments without prior validation are STRICTLY PROHIBITED.

**Rationale**: Prevents configuration errors that could impact compliance-required systems. Validation catches issues before they affect production.

**Rules**:
- Run `az deployment group validate` before every deployment
- Run `az deployment group what-if` to preview changes
- Document validation results in deployment logs
- Address all validation errors before proceeding

### IV. Security & Reliability First
Security and reliability best practices MUST be followed under all circumstances, even for legacy workloads.

**Rationale**: Compliance requirements mandate security controls regardless of workload age. Legacy status does not exempt from security obligations.

**Rules**:
- Enable Azure Monitor and diagnostic logs for all resources
- Apply network security groups and private endpoints where applicable
- Use managed identities instead of connection strings/keys
- Follow principle of least privilege for all access
- Enable Azure Security Center recommendations

### V. Minimal Naming with Type Identification
Resource names MUST be concise: minimal random characters for uniqueness + resource type identifier.

**Rationale**: Improves resource identification while respecting Azure naming limitations. Avoids verbose names that exceed character limits.

**Rules**:
- Format: `{resourceType}-{purpose}-{randomSuffix}`
- Example: `st-legacyvm-k7m3p` for storage account
- Respect Azure resource-specific length limits (e.g., storage: 24 chars, lowercase/numbers only)
- Random suffix: 4-6 alphanumeric characters
- Document naming pattern in infrastructure documentation

### VI. Region Standardization
All resources MUST deploy to US West 3 (westus3) region unless technically impossible.

**Rationale**: Centralizes resources for simplified management and cost tracking. Reduces complexity for legacy workloads with no multi-region requirements.

**Rules**:
- Default region parameter: `westus3`
- Document exceptions with technical justification
- Global resources (e.g., Azure Front Door) exempted by nature

## Infrastructure Standards

### Bicep Template Requirements
- Single main.bicep file as deployment entry point
- Use main.bicepparam for environment-specific parameters
- Leverage ARM dependency management (avoid explicit dependsOn unless necessary)
- Include detailed parameter descriptions and constraints
- Use Bicep decorators for validation (`@minLength`, `@maxLength`, `@allowed`)

### Module Management
- Reference AVM modules via Bicep Registry (br/public:avm/...)
- Pin to specific module versions (never use 'latest')
- Document module selection rationale in comments
- Review AVM module documentation for breaking changes during updates

### Documentation Requirements
- Maintain README.md with deployment instructions
- Document all parameters in main.bicepparam
- Include architecture diagram showing resource relationships
- Record compliance justifications for resource configurations

## Security & Compliance Requirements

### Mandatory Controls
- **Logging**: Enable diagnostic settings for all resources supporting it
- **Access Control**: Use Azure RBAC, no shared keys in parameters
- **Network Security**: Apply NSGs to subnet/NIC resources
- **Encryption**: Use Azure-managed encryption (minimum); customer-managed keys where compliance requires
- **Secrets Management**: Store sensitive values in Azure Key Vault, reference via Bicep getSecret()

### Compliance Documentation
- Tag all resources with compliance identifiers (e.g., `compliance: "legacy-retention"`)
- Document retention policies for data resources
- Record security exceptions with business justification
- Maintain audit trail of all infrastructure changes

### Prohibited Practices
- Hardcoded secrets or connection strings in Bicep files
- Public IP addresses without business justification
- Unrestricted network access (0.0.0.0/0 rules)
- Disabled diagnostic logging

## Development Workflow

### Pre-Deployment Phase
1. Research and select appropriate AVM modules
2. Draft Bicep templates with parameter documentation
3. Run local Bicep linting (`bicep build`)
4. Commit code to version control

### Validation Phase (MANDATORY GATE)
1. Run `az deployment group validate` and resolve all errors
2. Run `az deployment group what-if` and review changes
3. Document validation results
4. Obtain approval for resource changes (if required by organization)

### Deployment Phase
1. Deploy using validated parameters
2. Monitor deployment progress
3. Verify resource creation via Azure Portal/CLI
4. Test resource functionality
5. Document deployment outcomes

### Post-Deployment Phase
1. Verify diagnostic settings are active
2. Confirm tags applied correctly
3. Review security recommendations in Azure Security Center
4. Update documentation with deployed resource details

## Governance

This constitution supersedes all other development practices and guidelines. All infrastructure changes MUST comply with these principles.

### Amendment Process
1. Propose amendment with business/technical justification
2. Document impact on existing infrastructure
3. Update constitution with version increment following semantic versioning
4. Update all dependent templates and documentation
5. Communicate changes to all stakeholders

### Versioning Policy
- **MAJOR**: Breaking changes to core principles (e.g., removing AVM-only requirement)
- **MINOR**: New principle added or existing principle materially expanded
- **PATCH**: Clarifications, wording improvements, non-semantic fixes

### Compliance Review
All pull requests MUST verify compliance with this constitution. Constitution violations require explicit justification and approval exception.

Complexity that deviates from simplicity principles MUST be documented and justified with business or technical rationale.

**Version**: 1.0.0 | **Ratified**: 2026-01-27 | **Last Amended**: 2026-01-27

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the new constitution of your project, with a comment of something like Constitution added.

2. Specify

Spec Kit uses /speckit.specify to generate the spec.md file. Specifications can be evolved through iterating over the spec.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.specify and leveraging /speckit.clarify to review and challenge the specification.

Info

To learn more about what the specification should include, see the Specification chapter in the Spec Kit article.

  1. Run the following prompt to generate the specification for our example:
/speckit.specify Create specification, called "01-my-legacy-workload" for a legacy business application, running as a single virtual machine connected to a virtual network. The VM must run Windows Server 2016, needs to have at least 2 CPU cores, 8 GB of RAM, a standard HDD, and a 500 GB HDD-based data disk attached. It must be remotely accessible via a bastion host and needs to have access to an HDD-backed file share in a storage account connected via a private endpoint. The VM must access the internet via a NAT gateway. Network Security Groups (NSGs) must be created for each subnet, configured and assigned as applicable, restricting traffic to only what's necessary. VM subnet NSG must allow inbound RDP (port 3389) from Bastion subnet to enable bastion connectivity.

The VM's administrator password (created at the time of deployment) must be stored in a Key Vault, also deployed as part of this solution. The VM's administrator account must be called "vmadmin". The VM's computer name (netbios name) must be 15 or fewer characters long.

Always rely on parameters from the main.bicepparam file only. Have the name of the secret used for the admin password captured as a parameter. Include rich comments in both the main.bicep and main.bicepparam files to explain the purpose of each resource and parameter.

When a decision needs to be made on availability zones, always choose a number between 1 and 3 (never choose -1, that explicitly disables this feature).

Create everything in a single resource group, standing for a production environment. Do not create any additional environments (such as dev, test, staging, etc.).

Read the documentation (readme.md file) of each module you need to use to find out what parameters and complex parameter objects you can use. Don't guess the allowed parameters.

The VM must not be accessible from the internet and its logs should be captured in Log Analytics, included with the solution. Configure diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures).
βž• Expand to see the results

Notice that the execution of the /speckit.specify created a new file called requirements.md and a file called spec.md in the specs/001-legacy-vm-workload/ folder.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Specify Bootstrap Specify Bootstrap
<!-- markdownlint-disable -->
# Specification Quality Checklist: Legacy VM Workload Infrastructure

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-01-27
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Validation Results

**Status**: βœ… PASSED - All checklist items completed successfully

**Validation Date**: 2026-01-27

**Details**:
- Specification includes 5 user stories, properly prioritized (P1-P3)
- 16 functional requirements defined with concrete Azure resources
- 8 security & compliance requirements aligned with constitution
- 6 infrastructure constraints following project standards
- 5 monitoring & alerting requirements for operational visibility
- 11 success criteria, all measurable and deployment-focused
- Edge cases identified (6 scenarios)
- Assumptions documented (13 items)
- Out of scope clearly defined (13 items)
- No [NEEDS CLARIFICATION] markers - all requirements are concrete and actionable

**Readiness**: βœ… Specification is ready for `/speckit.plan` phase
<!-- markdownlint-disable -->
# Feature Specification: Legacy VM Workload Infrastructure

**Feature Branch**: `001-legacy-vm-workload`
**Created**: 2026-01-27
**Status**: Draft
**Input**: User description: "legacy business application, running as a single virtual machine connected to a virtual network with Windows Server 2016, 2 CPU cores, 8 GB RAM, standard HDD, 500 GB data disk, bastion access, file share via private endpoint, NAT gateway internet access, NSGs, Key Vault for VM password, Log Analytics with diagnostic logging and critical alerts"

## User Scenarios & Testing *(mandatory)*

<!--
  IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
  Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
  you should still have a viable MVP (Minimum Viable Product) that delivers value.

  Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
  Think of each story as a standalone slice of functionality that can be:
  - Developed independently
  - Tested independently
  - Deployed independently
  - Demonstrated to users independently
-->

### User Story 1 - Core VM Infrastructure Deployment (Priority: P1)

Deploy the fundamental infrastructure including virtual network, VM with required specifications, and basic connectivity. This establishes the baseline workload environment.

**Why this priority**: Without the VM and network infrastructure, no other components can function. This is the foundation for the entire workload.

**Independent Test**: Can be fully tested by deploying the infrastructure and verifying VM is created with correct specifications (Windows Server 2016, 2 cores, 8GB RAM, standard HDD) and can communicate within the VNet.

**Acceptance Scenarios**:

1. **Given** no existing infrastructure, **When** deployment is executed, **Then** VM is created with Windows Server 2016, 2 CPU cores, 8GB RAM
2. **Given** deployment is complete, **When** checking VM configuration, **Then** VM has standard HDD OS disk and is placed in correct VNet
3. **Given** VM is deployed, **When** checking computer name, **Then** NetBIOS name is 15 characters or fewer

---

### User Story 2 - Secure Storage and Data Disk (Priority: P2)

Provision the 500GB data disk for the VM and configure the storage account with file share accessible via private endpoint. This provides the data storage layer for the application.

**Why this priority**: Data storage is critical for application functionality but depends on the VM infrastructure being in place first.

**Independent Test**: Can be tested by verifying the 500GB HDD data disk is attached to the VM and the file share is accessible from the VM through the private endpoint.

**Acceptance Scenarios**:

1. **Given** VM infrastructure exists, **When** data disk deployment executes, **Then** 500GB HDD-based managed disk is attached to the VM
2. **Given** storage account is deployed, **When** checking storage configuration, **Then** HDD-backed file share is created
3. **Given** private endpoint is deployed, **When** VM attempts to access file share, **Then** connection succeeds through private network without traversing internet

---

### User Story 3 - Secure Access and Secrets Management (Priority: P2)

Implement bastion host for secure remote access and Key Vault for storing the VM administrator password. This ensures secure access patterns for operations teams.

**Why this priority**: Secure access is essential for ongoing operations but the infrastructure must exist before access can be configured.

**Independent Test**: Can be tested by connecting to the VM through bastion host using credentials retrieved from Key Vault.

**Acceptance Scenarios**:

1. **Given** bastion and Key Vault are deployed, **When** VM administrator password is generated at deployment, **Then** password is stored in Key Vault secret
2. **Given** bastion host is deployed, **When** operator attempts to connect to VM, **Then** connection succeeds through bastion without public IP on VM
3. **Given** Key Vault access is configured, **When** retrieving VM password, **Then** secret can be accessed only by authorized identities
4. **Given** VM subnet NSG is configured, **When** bastion attempts RDP connection to VM, **Then** traffic is allowed through NSG rule (port 3389 from Bastion subnet)

---

### User Story 4 - Internet Connectivity and Network Security (Priority: P3)

Configure NAT gateway for outbound internet access and implement Network Security Groups for all subnets with least-privilege rules.

**Why this priority**: Network security controls are important but the workload can function for testing without full NSG configuration initially.

**Independent Test**: Can be tested by verifying VM can reach internet through NAT gateway and that NSG rules block unauthorized traffic.

**Acceptance Scenarios**:

1. **Given** NAT gateway is deployed, **When** VM initiates outbound internet connection, **Then** traffic routes through NAT gateway
2. **Given** NSGs are configured, **When** unauthorized traffic attempts to reach VM, **Then** traffic is blocked by NSG rules
3. **Given** NSGs are deployed, **When** checking subnet associations, **Then** each subnet has appropriate NSG assigned

---

### User Story 5 - Monitoring and Alerting (Priority: P3)

Deploy Log Analytics workspace, configure diagnostic settings for all resources, and set up critical alerts (VM stopped, disk full, Key Vault access failures).

**Why this priority**: Monitoring is important for operations but the workload can function without it. It provides operational visibility rather than core functionality.

**Independent Test**: Can be tested by verifying diagnostic logs are flowing to Log Analytics and triggering test scenarios that generate alerts.

**Acceptance Scenarios**:

1. **Given** Log Analytics workspace is deployed, **When** resources are created, **Then** diagnostic settings send logs to workspace
2. **Given** alerting is configured, **When** VM is stopped, **Then** critical alert is triggered
3. **Given** alerting is configured, **When** Key Vault access fails, **Then** critical alert is triggered
4. **Given** diagnostic logging is active, **When** querying Log Analytics, **Then** logs are available within 5 minutes

---

## Clarifications

### Session 2026-01-27

- Q: VNet address space and subnet sizing for VM, bastion, and private endpoint subnets? β†’ A: VNet: 10.0.0.0/24, VM subnet: 10.0.0.0/27, Bastion subnet: 10.0.0.64/26, Private endpoint subnet: 10.0.0.128/27
- Q: Storage file share quota size? β†’ A: 1024 GiB (1 TiB)
- Q: Disk space alert threshold percentage? β†’ A: 85% full
- Q: Alert notification method for critical alerts? β†’ A: Azure Portal notifications only
- Q: VM size SKU for 2 cores and 8GB RAM requirement? β†’ A: Standard_D2s_v3

### Edge Cases

- What happens when VM computer name parameter would exceed 15 characters? (NetBIOS limit must be enforced)
- How does deployment handle when Key Vault secret name parameter is not provided or is invalid?
- What happens when storage account name would exceed 24 characters or contains invalid characters?
- How does system handle when no availability zone is specified for resources requiring zone selection?
- What happens when private endpoint deployment fails but storage account succeeds?
- How does deployment handle if bastion subnet already exists in the VNet from a previous deployment?

## Requirements *(mandatory)*

<!--
  ACTION REQUIRED: The content in this section represents placeholders.
  Fill them out with the right functional requirements.
-->

### Functional Requirements

<!-- NOTE: "Standard HDD" refers to Azure Standard_LRS disk SKU (magnetic disk storage) -->

- **FR-001**: Infrastructure MUST provision a Windows Server 2016 Virtual Machine with size Standard_D2s_v3 (2 vCPUs, 8 GiB RAM) using Standard HDD for OS disk
- **FR-002**: Infrastructure MUST attach a 500GB HDD-based managed disk to the VM as a data disk
- **FR-003**: Infrastructure MUST create Virtual Network with address space 10.0.0.0/24 containing three subnets: VM subnet (10.0.0.0/27), Bastion subnet (10.0.0.64/26), and private endpoint subnet (10.0.0.128/27)
- **FR-004**: Infrastructure MUST deploy Azure Bastion for secure remote access to the VM without public IP
- **FR-005**: Infrastructure MUST provision Storage Account with HDD-backed file share (1024 GiB quota) accessible via private endpoint
- **FR-006**: Infrastructure MUST deploy NAT Gateway for VM outbound internet connectivity
- **FR-007**: Infrastructure MUST create Network Security Groups for each subnet with least-privilege rules
- **FR-008**: Infrastructure MUST deploy Azure Key Vault to store VM administrator password
- **FR-009**: Infrastructure MUST set VM administrator account name to "vmadmin"
- **FR-010**: Infrastructure MUST ensure VM computer name (NetBIOS name) is 15 characters or fewer
- **FR-011**: Infrastructure MUST generate and store VM administrator password in Key Vault at deployment time
- **FR-012**: Infrastructure MUST accept Key Vault secret name as a parameter from main.bicepparam
- **FR-013**: Infrastructure MUST deploy all resources to a single resource group representing production environment
- **FR-014**: Infrastructure MUST select availability zone between 1-3 for zone-capable resources (never use -1)
- **FR-015**: Infrastructure MUST include rich comments in both main.bicep and main.bicepparam explaining resource purpose and parameters
- **FR-016**: Infrastructure MUST rely exclusively on parameters defined in main.bicepparam file

### Security & Compliance Requirements (Mandatory for all features)

- **SEC-001**: All resources MUST enable diagnostic settings and send logs to Log Analytics Workspace
- **SEC-002**: VM MUST use managed identity for Azure resource authentication (no connection strings/keys in configuration)
- **SEC-003**: Network Security Groups MUST restrict traffic to only necessary ports and protocols per subnet
- **SEC-003a**: VM subnet NSG MUST allow inbound RDP (port 3389) from Bastion subnet (10.0.0.64/26) to enable bastion connectivity
- **SEC-004**: All resources MUST be tagged with compliance identifier "legacy-retention"
- **SEC-005**: VM administrator password MUST be stored in Azure Key Vault, never in code or parameters
- **SEC-006**: VM MUST NOT have public IP address assigned (access only through bastion)
- **SEC-007**: Storage account file share MUST be accessible only through private endpoint, not public endpoint
- **SEC-008**: Key Vault MUST restrict access to only authorized identities using RBAC

### Infrastructure Constraints

- **IC-001**: MUST deploy to westus3 region (US West 3)
- **IC-002**: MUST use Azure Verified Modules (AVM) exclusively (read module readme.md for parameter documentation)
- **IC-003**: MUST validate deployment with `az deployment group validate` before applying
- **IC-004**: MUST run `az deployment group what-if` to preview changes
- **IC-005**: Resource names MUST follow pattern: {resourceType}-{purpose}-{random4-6chars}
- **IC-006**: MUST NOT create additional environments (dev, test, staging) - production only

### Monitoring & Alerting Requirements

- **MON-001**: Infrastructure MUST deploy Log Analytics workspace for centralized logging
- **MON-002**: Infrastructure MUST configure diagnostic logging for VM, Key Vault, Storage Account, and network resources
- **MON-003**: Infrastructure MUST create critical alert for VM stopped/deallocated condition (Portal notifications)
- **MON-004**: Infrastructure MUST create critical alert for disk space exceeding 85% threshold (Portal notifications)
- **MON-005**: Infrastructure MUST create critical alert for Key Vault access failures (Portal notifications)

### Key Azure Resources

- **Virtual Machine**: Windows Server 2016 VM with size Standard_D2s_v3 (2 vCPUs, 8 GiB RAM), Standard HDD OS disk, managed identity enabled
- **Managed Disk**: 500GB HDD-based data disk attached to VM
- **Virtual Network**: VNet with subnets for VM, bastion, and private endpoints
- **Azure Bastion**: Secure RDP access to VM without public IP
- **Storage Account**: Standard HDD storage with file share
- **Private Endpoint**: Secure connectivity between VM and storage account file share
- **NAT Gateway**: Outbound internet connectivity for VM subnet
- **Network Security Groups**: One per subnet with least-privilege rules
- **Key Vault**: Stores VM administrator password as secret
- **Log Analytics Workspace**: Centralized logging for all resources
- **Azure Monitor Alerts**: Critical alerts for VM stopped, disk full, Key Vault access failures

## Success Criteria *(mandatory)*

<!--
  ACTION REQUIRED: Define measurable success criteria.
  These must be technology-agnostic and measurable.
-->

### Measurable Outcomes

- **SC-001**: Infrastructure deploys successfully within 20 minutes including all resources
- **SC-002**: ARM validation (`az deployment group validate`) passes without errors
- **SC-003**: ARM what-if analysis shows all expected resources will be created
- **SC-004**: VM is accessible via bastion host within 5 minutes of deployment completion
- **SC-005**: VM can access file share through private endpoint connection
- **SC-006**: VM can reach internet through NAT gateway for outbound connections
- **SC-007**: Diagnostic logs from all resources appear in Log Analytics within 5 minutes
- **SC-008**: All resources pass Azure Security Center baseline compliance checks
- **SC-009**: NSG rules successfully block unauthorized traffic in test scenarios
- **SC-010**: VM administrator password can be retrieved from Key Vault by authorized identities
- **SC-011**: Critical alerts can be triggered and verified (VM stop, simulated disk full warning, Key Vault access attempt)

## Assumptions

- Azure subscription has sufficient quota for Standard_D2s_v3 VM size
- Azure Bastion service is available in westus3 region
- Windows Server 2016 image is available in Azure Marketplace for westus3 region
- Log Analytics workspace can be deployed in westus3 region
- Private endpoint feature is available for storage accounts in westus3 region
- NAT Gateway is available in westus3 region
- Deployment is executed by identity with sufficient permissions to create all resource types
- Resource group name will be provided as parameter in main.bicepparam
- Random suffix for resource names will be generated or provided as parameter
- Default log retention period of 30 days is acceptable for Log Analytics (compliance requirement may differ)
- Standard_LRS storage redundancy is acceptable for this legacy workload
- VM will be deployed without availability sets or scale sets (single instance acceptable)
- Availability zone selection (1, 2, or 3) will be provided as parameter

## Out of Scope

- Multi-region deployment or disaster recovery configuration
- High availability (availability sets, load balancers, multiple VMs)
- Auto-scaling capabilities
- Backup and restore automation (Azure Backup configuration)
- Additional environments (development, test, staging)
- Application installation or configuration on the VM
- Custom monitoring dashboards or complex alerting logic beyond critical alerts
- Network connectivity to on-premises networks (VPN or ExpressRoute)
- Azure Active Directory domain join
- Additional data disks beyond the single 500GB disk specified
- Storage account configuration beyond file share (no blob containers, tables, or queues)
- Advanced network features (Azure Firewall, Application Gateway, Traffic Manager)
- Cost optimization recommendations or reserved instance planning

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the clarified specification of your project, with a comment of something like Specification created.

3. Clarify (Optional)

Spec Kit uses /speckit.clarify to generate adjust information captured in spec.md. The prompt doesn’t require any specific inputs as it analyzes the existing specification for gaps.

Info

To learn more about the clarify step, see the Clarify chapter in the Spec Kit article.

  1. Run the following prompt to generate clarification questions for our example:
/speckit.clarify
βž• Expand to see example questions

The clarify phase iterates on the spec.md file by asking questions, making suggestions and capturing the user’s feedback.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the clarify prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like. You can answer in the following format, e.g.: Q1: E, Q2:A, Q3:A
Specify Bootstrap

In the Copilot chat window, you will likely see some questions raised, similar to these. You can answer these just like in a normal chat conversation - e.g., by typing the letter standing for the option provided for each question, or by elaborating further if needed.

Specify Bootstrap Specify Bootstrap Specify Bootstrap

See a few examples of clarifying questions Copilot may ask. Copilot typically suggests a few options, but you can always deviate from them as needed, just use the chat to provide your answers.

## Clarifications

- Q: How should the Azure file share be mounted on the Windows VM? β†’ A: Post-deployment manual mount by administrator following documented procedure (no automation, aligns with IaC-first principle)
- Q: What level of monitoring and alerting should be configured for this legacy workload? β†’ A: Diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures)
- Q: If the initial deployment partially fails (e.g., VM creates but Bastion fails), what should the recovery procedure be? β†’ A: Keep existing resources, fix errors in template/parameters, redeploy entire template (ARM incremental mode handles already-deployed resources)
- Q: File share initial quota and growth strategy? β†’ A: 1TB initial quota with documented growth monitoring procedure
- Q: VM administrator username? β†’ A: vmadmin
- Q: VNet address space and subnet sizing for VM, bastion, and private endpoint subnets? β†’ A: VNet: 10.0.0.0/24, VM subnet: 10.0.0.0/27, Bastion subnet: 10.0.0.64/26, Private endpoint subnet: 10.0.0.128/27
- Q: Storage file share quota size? β†’ A: 1024 GiB (1 TiB)
- Q: Disk space alert threshold percentage? β†’ A: 85% full
- Q: Alert notification method for critical alerts? β†’ A: Azure Portal notifications only
- Q: VM size SKU for 2 cores and 8GB RAM requirement? β†’ A: Standard_D2s_v3

Coming soon!

  1. Review and approve the changes suggested by Copilot by clicking on the “Keep” button!
  2. It is recommended to make a commit now to capture the updated specification of your project, with a comment of something like Specification clarified.

4. Plan

Spec Kit uses /speckit.plan to generate the plan.md file. The plan can be evolved through iterating over the plan.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.plan, or leveraging /speckit.checklist to review/validate and challenge the plan.

Info

To learn more about what the plan should include, see the Plan chapter in the Spec Kit article.

Click through the tabs to see the details!

  1. Run the following prompt to generate the plan for our example:
/speckit.planΒ Create a detailed plan for the spec. Build with the latest version of Bicep and the latest available version of each AVM module. Use the "Bicep/list_avm_metadata" MCP tool to find out what's the latest version of each module. Only include direct resource references in the Bicep template if no related AVM resource modules are available. Similarly, for diagnostic settings, role assignments, resource locks, tags, managed identities, private endpoints, customer manged keys, etc., always use the related "interface" built-in to each resource module when available. Do not create and reference local modules, or any other bicep files.Β If a subset of the deployments fail, don't delete anything, just attempt redeploying the whole solution after fixing any bugs. Create a single main.bicep file, with direct references to AVM modules and leverage a single *.bicepparam file for all input parameters.

When generating the admin password for the VM, use the secret feature built into the AVM Key Vault module. Leverage the uniqueString function to generate a new random password and do not use any external helper script (including deployment scripts) for generating the password. Provide this password to the VM module by referencing the Key vault secret that stores it. The template must first generate this password including a random, complex string, using the uniqueString Bicep function, store it in Key Vault and then reference it for the VM to use it as admin password at deployment time.

Don't connect the file share to the VM just yet - i.e., no need to extract storage keys or shared access signatures - we will do this later.

If implementing resource level locks, always use the built-in AVM "interface" for resource locks, instead of directly deploying the "Microsoft.Authorization/locks" resource.

Bicep template must compile without warnings or errors using the latest stable Bicep CLI version. Create a bicepconfig.json file to generate a warning when not the latest version of an AVM module is used. You can configure this by making sure in the bicepconfig.json file, there is a node under analyzers/core/rules/use-recent-module-versions/level" with the value of "warning". Before validating the template or attempting the first deployment, always fix all warnings or errors related to the AVM module versioning by updating to the latest available version of each module.
βž• Expand to see the results

Notice how the plan step creates the plan.md file and a number of additional helper files. These may very depending on your prompts, the solution you are building, the version of Spec Kit and the LLM used. These typically include: data-model.md, research.md, quickstart.md and optional files in the contracts folder, such as outputs.md and parameters.md.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Implementation Plan: Legacy VM Workload

**Branch**: `001-legacy-vm-workload` | **Date**: 2026-01-27 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-legacy-vm-workload/spec.md`

**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.

## Summary

Deploy a legacy Windows Server 2016 virtual machine workload to Azure using Infrastructure-as-Code (Bicep) with the following capabilities:

- **Core Infrastructure**: Windows Server 2016 VM (Standard_D2s_v3) in availability zone 1, with system-assigned managed identity, 500GB HDD data disk, no public IP, deployed in dedicated VNet (10.0.0.0/24)
- **Secure Access**: Azure Bastion for RDP access, password stored in Key Vault, no direct internet exposure
- **Storage**: 1TB Azure Files share accessible via private endpoint from VM subnet
- **Network Security**: NAT Gateway for outbound internet, NSGs on all subnets with least-privilege rules, private endpoint for storage account
- **Monitoring**: Log Analytics workspace with diagnostic settings on all resources, three critical alerts (VM stopped, disk space >85%, Key Vault access failures), portal-only notifications
- **Naming**: Minimal naming convention with resource type prefix and 4-6 character random suffix
- **Region**: All resources in westus3 (US West 3)

**Technical Approach**: Single-template Bicep deployment using 12 Azure Verified Modules (latest stable versions), ARM-managed dependencies, parameter-driven configuration with uniqueString() for password generation.

## Technical Context

**IaC Language**: Bicep v0.33.0 or later (latest stable)
**Module Framework**: Azure Verified Modules (AVM) - 12 modules identified
**Target Region**: westus3 (US West 3)
**Deployment Tool**: Azure CLI v2.65.0+ (`az deployment group create`)
**Validation Required**: `bicep build` + `az deployment group validate` + `what-if` analysis
**Workload Type**: Legacy compliance-retained workload (Windows Server 2016)
**High Availability**: Single-zone deployment (availability zone parameter: 1, 2, or 3)
**Disaster Recovery**: Not required
**Scalability Requirements**: Static single VM, no auto-scaling
**Security Baseline**: Diagnostic logging to Log Analytics, managed identities, NSGs, private endpoints, Key Vault for secrets
**Naming Convention**: `{resourceType}-{purpose}-{random4-6chars}` (e.g., `vm-legacyvm-k7m3p`)
**Compliance Tags**: `workload: legacy-vm`, `environment: production`, `compliance: legacy-retention`

### AVM Modules Selected (Latest Versions)

| Module | Version | Purpose |
|--------|---------|---------|
| avm/res/network/virtual-network | 0.7.2 | VNet with 3 subnets (VM, Bastion, PE) |
| avm/res/compute/virtual-machine | 0.21.0 | Windows Server 2016 VM with data disk |
| avm/res/network/bastion-host | 0.8.2 | Secure RDP access |
| avm/res/storage/storage-account | 0.31.0 | File share with private endpoint |
| avm/res/network/nat-gateway | 2.0.1 | Outbound internet connectivity |
| avm/res/network/network-security-group | 0.5.2 | Subnet-level network security (3 NSGs) |
| avm/res/key-vault/vault | 0.13.3 | Store VM admin password |
| avm/res/operational-insights/workspace | 0.15.0 | Centralized logging |
| avm/res/network/private-endpoint | 0.11.1 | Private storage access |
| avm/res/insights/metric-alert | 0.4.1 | Monitoring alerts (3 alerts) |
| avm/res/network/private-dns-zone | 0.8.0 | DNS for private endpoints |

**Module Documentation**: See [research.md](./research.md) for detailed module analysis and alternatives considered.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- [x] **I. Infrastructure-as-Code First**: βœ… All resources defined in single `main.bicep` template, no manual Portal configurations, deployment via Azure CLI only
- [x] **II. AVM-Only Modules**: βœ… Using 12 AVM modules with latest stable versions (0.7.2 to 2.0.1), no direct resource declarations except where AVM unavailable (none identified)
- [x] **III. Validation Before Deployment**: βœ… Quickstart guide includes `bicep build`, `az deployment group validate`, and `what-if` analysis steps before deployment
- [x] **IV. Security & Reliability First**: βœ… Diagnostic logs to Log Analytics for all resources, managed identities (VM system-assigned), NSGs on all subnets, private endpoint for storage, Key Vault for password, least-privilege RBAC
- [x] **V. Minimal Naming with Type ID**: βœ… Naming pattern: `{type}-{purpose}-{random}` (e.g., `vm-legacyvm-k7m3p`, `kv-legacyvm-k7m3p`), random suffix via `uniqueString(resourceGroup().id)` - 6 chars
- [x] **VI. Region Standardization**: βœ… All resources deploy to westus3, parameter default set to `'westus3'`, no exceptions needed

**Constitution Compliance Status**: **PASSED** βœ…

All 6 constitution principles satisfied. No violations requiring justification.

## Project Structure

### Documentation (this feature)

## Project Structure

### Documentation (this feature)

```text
specs/001-legacy-vm-workload/
β”œβ”€β”€ spec.md              # Feature specification (completed)
β”œβ”€β”€ plan.md              # This file - implementation plan (in progress)
β”œβ”€β”€ research.md          # AVM module research (Phase 0 - completed)
β”œβ”€β”€ data-model.md        # Network topology & resource model (Phase 1 - completed)
β”œβ”€β”€ quickstart.md        # Deployment guide (Phase 1 - completed)
β”œβ”€β”€ checklists/
β”‚   └── requirements.md  # Quality validation checklist (passed)
└── tasks.md             # Task decomposition (Phase 2 - not yet created)
```

### Infrastructure Code Structure

```text
avm-workload/
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ main.bicep           # Main deployment template (to be created)
β”‚   β”œβ”€β”€ main.bicepparam      # Deployment parameters (to be created)
β”‚   └── bicepconfig.json     # Bicep analyzer configuration (to be created)
β”œβ”€β”€ specs/
β”‚   └── 001-legacy-vm-workload/  # This feature documentation
β”œβ”€β”€ .specify/
β”‚   β”œβ”€β”€ memory/
β”‚   β”‚   └── constitution.md  # Governance framework
β”‚   β”œβ”€β”€ templates/           # SpecKit templates
β”‚   └── scripts/             # SpecKit automation scripts
└── README.md                # Project overview (to be created)
```

**Structure Decision**: Single-template Infrastructure-as-Code approach using Bicep with Azure Verified Modules. All 20+ resources (VM, VNet, Bastion, Storage, Key Vault, NSGs, NAT Gateway, Log Analytics, Alerts, Private Endpoint, DNS Zone, diagnostic settings, RBAC assignments) defined in one `main.bicep` file with ARM handling dependency ordering automatically. No custom modules needed - all functionality provided by AVM modules.

## Complexity Tracking

**No Constitution Violations** - This section is intentionally empty.

All 6 constitution principles are satisfied with no exceptions required. See Constitution Check section above for detailed compliance status.

---

## Implementation Phases

### Phase 0: Research & Architecture βœ… COMPLETED

**Objective**: Identify required AVM modules, resolve technical unknowns, document architectural decisions.

**Artifacts Created**:
- βœ… [research.md](./research.md) - AVM module inventory with latest versions, alternatives considered, implementation patterns
- βœ… [data-model.md](./data-model.md) - Network topology, resource dependencies, configuration model, security model
- βœ… [quickstart.md](./quickstart.md) - Step-by-step deployment guide with validation commands

**Key Decisions**:
1. **VM Password Generation**: Use Bicep `uniqueString()` function with multiple seeds (resourceGroup().id, deployment().name) - no external scripts needed
2. **Network Addressing**: VNet 10.0.0.0/24 with VM subnet /27, Bastion subnet /26, PE subnet /27
3. **Storage Access**: Private endpoint with private DNS zone integration (privatelink.file.core.windows.net)
4. **Naming Pattern**: `{type}-{purpose}-{uniqueString(6)}` for all resources
5. **Dependency Management**: Single-template approach, let ARM handle resource ordering automatically
6. **Diagnostic Settings**: All resources send logs/metrics to centralized Log Analytics workspace

**Unknowns Resolved**:
- βœ… VNet sizing: 10.0.0.0/24 confirmed sufficient (256 IPs)
- βœ… File share quota: 1TB (1024 GiB)
- βœ… Disk alert threshold: 85%
- βœ… Notification method: Azure Portal only (no Action Groups)
- βœ… VM size: Standard_D2s_v3
- βœ… AVM modules exist for all 11 required Azure resource types

### Phase 1: Infrastructure Code Implementation πŸ”„ IN PROGRESS

**Objective**: Create Bicep templates with AVM module references, parameter file, and configuration.

#### Task 1.1: Create bicepconfig.json ⏳ PENDING

**File**: `infra/bicepconfig.json`

**Purpose**: Configure Bicep analyzer to enforce AVM best practices and warn on outdated module versions.

**Configuration**:
```json
{
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "use-recent-module-versions": {
          "level": "warning"
        }
      }
    }
  },
  "moduleAliases": {
    "br": {
      "public": {
        "registry": "mcr.microsoft.com",
        "modulePath": "bicep"
      }
    }
  }
}
```

**Validation**: Run `bicep build main.bicep` and verify no analyzer warnings.

#### Task 1.2: Create main.bicep ⏳ PENDING

**File**: `infra/main.bicep`

**Purpose**: Single deployment template referencing 12 AVM modules with proper parameters.

**Structure** (700-900 lines estimated):

1. **Header** (lines 1-30):
  - Metadata: name, description, owner
  - Target scope: `targetScope = 'resourceGroup'`
  - Parameters: vmSize, vmAdminUsername, availabilityZone, fileShareQuotaGiB, logAnalyticsRetentionDays

2. **Variables** (lines 31-80):
  - Random suffix: `var suffix = uniqueString(resourceGroup().id)`
  - Resource names: all following `{type}-{purpose}-${suffix}` pattern
  - VM password: `var vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name)}'`
  - Network configuration: subnet CIDR blocks, NSG rules
  - Tags: workload, environment, compliance, managedBy, deploymentDate

3. **Log Analytics Workspace** (lines 81-110):
  ```bicep
  module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.15.0' = {
    name: 'deploy-log-analytics'
    params: {
      name: 'law-legacyvm-${suffix}'
      location: location
      retentionInDays: logAnalyticsRetentionDays
      tags: tags
    }
  }
  ```

4. **Virtual Network** (lines 111-200):
  - Module: `avm/res/network/virtual-network:0.7.2`
  - 3 subnets: VM (10.0.0.0/27), Bastion (10.0.0.64/26), PE (10.0.0.128/27)
  - Diagnostic settings to Log Analytics

5. **Network Security Groups** (lines 201-350):
  - Module: `avm/res/network/network-security-group:0.5.2` (3 instances)
  - NSG 1: VM subnet (deny all inbound, allow internet + VNet outbound)
  - NSG 2: Bastion subnet (standard Azure Bastion rules)
  - NSG 3: PE subnet (allow VM subnet inbound on 445, allow all outbound)
  - Associate each NSG with its subnet
  - Diagnostic settings to Log Analytics

6. **NAT Gateway** (lines 351-380):
  - Module: `avm/res/network/nat-gateway:2.0.1`
  - Public IP auto-created
  - Associate with VM subnet
  - Diagnostic settings to Log Analytics

7. **Azure Bastion** (lines 381-410):
  - Module: `avm/res/network/bastion-host:0.8.2`
  - Depends on VNet and Bastion NSG
  - Public IP auto-created
  - Diagnostic settings to Log Analytics

8. **Key Vault** (lines 411-470):
  - Module: `avm/res/key-vault/vault:0.13.3`
  - SKU: Standard
  - Access model: RBAC
  - Secret: VM admin password (generated variable)
  - RBAC assignment: VM managed identity β†’ Key Vault Secrets User role
  - Diagnostic settings to Log Analytics

9. **Private DNS Zone** (lines 471-500):
  - Module: `avm/res/network/private-dns-zone:0.8.0`
  - Zone name: `privatelink.file.core.windows.net`
  - VNet link to main VNet
  - Depends on VNet

10. **Storage Account** (lines 501-580):
    - Module: `avm/res/storage/storage-account:0.31.0`
    - Kind: StorageV2, SKU: Standard_LRS
    - Public network access: Disabled
    - File share: 1024 GiB quota
    - Diagnostic settings to Log Analytics

11. **Private Endpoint** (lines 581-620):
    - Module: `avm/res/network/private-endpoint:0.11.1`
    - Service: file
    - Subnet: Private Endpoint subnet
    - DNS integration: Private DNS zone
    - Depends on Storage Account, VNet, Private DNS Zone

12. **Virtual Machine** (lines 621-730):
    - Module: `avm/res/compute/virtual-machine:0.21.0`
    - OS: Windows Server 2016
    - Size: Standard_D2s_v3
    - Admin username: parameter
    - Admin password: Key Vault secret reference
    - System-assigned managed identity
    - OS disk: Standard HDD
    - Data disk: 500GB Standard HDD, LUN 0
    - NIC: VM subnet, dynamic private IP, no public IP
    - Availability zone: parameter (1, 2, or 3)
    - Diagnostic settings to Log Analytics
    - Depends on VNet, Key Vault

13. **Metric Alerts** (lines 731-850):
    - Module: `avm/res/insights/metric-alert:0.4.1` (3 instances)
    - Alert 1: VM stopped (CPU < 1% for 15 min, Sev 0)
    - Alert 2: Disk space >85% (OS disk used %, Sev 0)
    - Alert 3: Key Vault access failures (SecretGet failures, Sev 0)
    - No action groups (portal-only notifications)
    - Depends on VM and Key Vault

14. **Outputs** (lines 851-900):
    - VM name and resource ID
    - Key Vault name and resource ID
    - Storage account name and file share name
    - Bastion name
    - Log Analytics workspace ID
    - VNet name and resource ID

**Key Implementation Notes**:
- Use AVM module built-in interfaces for diagnostic settings (NOT direct `Microsoft.Insights/diagnosticSettings` resources)
- Use AVM module built-in interfaces for RBAC role assignments (NOT direct `Microsoft.Authorization/roleAssignments` resources)
- Use AVM module built-in interfaces for locks if needed (NOT direct `Microsoft.Authorization/locks` resources)
- Reference Key Vault secret for VM password using AVM module's secret reference parameter
- ARM will automatically determine deployment order based on dependencies
- All resource names use `uniqueString(resourceGroup().id)` for suffix (6 chars, consistent across all resources)
- Storage account name: no hyphens (Azure requirement), format: `st${replace(suffix, '-', '')}` (max 24 chars)
- VM computer name: max 15 chars, format: `vm-${substring(suffix, 0, 10)}`

#### Task 1.3: Create main.bicepparam ⏳ PENDING

**File**: `infra/main.bicepparam`

**Purpose**: Parameter file for deployment with sensible defaults.

**Content**:
```bicep
using './main.bicep'

// VM Configuration
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param availabilityZone = 1

// Storage Configuration
param fileShareQuotaGiB = 1024

// Monitoring Configuration
param logAnalyticsRetentionDays = 30

// Optional overrides (uncomment to customize)
// param vmName = 'vm-custom-name'
// param keyVaultName = 'kv-custom-name'
```

**Validation**: Ensure all parameters match those defined in `main.bicep`.

#### Task 1.4: Create project README.md ⏳ PENDING

**File**: `README.md` (root of repository)

**Purpose**: Project overview with quickstart and links to detailed documentation.

**Sections**:
1. Project Overview
2. Architecture Summary (link to data-model.md)
3. Prerequisites (link to quickstart.md)
4. Quick Deployment (3-step process)
5. Documentation Links (spec.md, plan.md, research.md, quickstart.md)
6. Governance (link to constitution.md)
7. Support and Contributing

### Phase 2: Validation & Testing πŸ”„ NEXT PHASE

**Objective**: Validate Bicep templates and perform what-if analysis before deployment.

#### Task 2.1: Bicep Build Validation

```powershell
cd infra
bicep build main.bicep
# Expected: main.json created, zero warnings
```

**Success Criteria**:
- No compilation errors
- No analyzer warnings
- Output ARM JSON file created successfully

#### Task 2.2: ARM Template Validation

```powershell
az group create --name rg-legacyvm-test --location westus3

az deployment group validate `
  --resource-group rg-legacyvm-test `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose
```

**Success Criteria**:
- Validation passes with `provisioningState: Succeeded`
- No errors related to missing resource providers
- No errors related to invalid parameters

#### Task 2.3: What-If Analysis

```powershell
az deployment group what-if `
  --resource-group rg-legacyvm-test `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose
```

**Success Criteria**:
- All expected resources show as "Create" (green +)
- No unexpected deletions or modifications
- Resource count: 20-25 resources total
- No warnings about deprecated API versions

#### Task 2.4: Code Review Checklist

Manual review of `main.bicep`:

- [ ] All 12 AVM modules referenced with exact versions (no floating versions)
- [ ] All module versions match research.md documentation
- [ ] uniqueString() used consistently for resource name suffixes
- [ ] Storage account name respects 24-char limit and no-hyphen requirement
- [ ] VM computer name respects 15-char limit
- [ ] All subnets have NSG associations
- [ ] NAT Gateway associated with VM subnet only
- [ ] Bastion deployed to AzureBastionSubnet (exact name required)
- [ ] **VM subnet NSG allows inbound RDP (port 3389) from Bastion subnet (10.0.0.64/26) - CRITICAL for bastion connectivity**
- [ ] Private endpoint connects to correct storage service (file)
- [ ] Private DNS zone has VNet link
- [ ] Key Vault secret created with generated password
- [ ] VM references Key Vault secret for password (not plaintext)
- [ ] VM has system-assigned managed identity
- [ ] RBAC assignment: VM identity β†’ Key Vault (Key Vault Secrets User role)
- [ ] All resources have diagnostic settings to Log Analytics
- [ ] All resources have tags applied
- [ ] All resources deploy to westus3 region
- [ ] VM data disk configured: 500GB, Standard HDD, LUN 0
- [ ] File share quota: 1024 GiB
- [ ] 3 metric alerts configured with correct thresholds
- [ ] No action groups on alerts (portal-only requirement)
- [ ] Comments explain each major section

### Phase 3: Deployment πŸ”„ FUTURE PHASE

**Objective**: Deploy infrastructure to Azure and verify all resources operational.

#### Task 3.1: Initial Deployment

**Pre-deployment**:
- Create resource group: `rg-legacyvm-prod`
- Ensure subscription quotas sufficient (VMs, Public IPs, etc.)
- Authenticate to Azure CLI with sufficient permissions

**Deployment Command**:
```powershell
az deployment group create `
  --name "legacyvm-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
  --resource-group rg-legacyvm-prod `
  --template-file infra/main.bicep `
  --parameters infra/main.bicepparam `
  --verbose
```

**Expected Duration**: 15-20 minutes

**Monitoring**: Track deployment progress in Azure Portal β†’ Resource Groups β†’ rg-legacyvm-prod β†’ Deployments

#### Task 3.2: Post-Deployment Verification

Follow checklist in [quickstart.md](./quickstart.md):

1. **Resource Count Verification**:
  ```powershell
  az resource list --resource-group rg-legacyvm-prod --output table
  # Expected: 20-25 resources
  ```

2. **Bastion Connectivity Test**:
  - Retrieve VM password from Key Vault
  - Connect to VM via Azure Portal Bastion
  - Verify Windows Server 2016 desktop loads

3. **Log Analytics Verification**:
  - Run sample Kusto queries
  - Verify logs appearing for all resources
  - Check for any error logs

4. **Network Connectivity Tests** (from VM):
  - Test internet access via NAT Gateway
  - Verify private endpoint DNS resolution
  - Ping storage account private IP

5. **Alert Verification**:
  - Trigger test alert (Key Vault access failure)
  - Verify alert visible in Azure Portal within 5-10 minutes
  - Confirm alert severity (Sev 0)

#### Task 3.3: Documentation Updates

- Update README.md with actual deployed resource names
- Record deployment timestamp and duration
- Document any deployment issues encountered and resolutions
- Create CHANGELOG.md entry for initial deployment

### Phase 4: Operational Handoff πŸ”„ FUTURE PHASE

**Objective**: Provide operational documentation and ensure supportability.

#### Task 4.1: Operational Runbooks

Create runbooks for common operations:
- VM start/stop procedures
- File share quota increase
- Bastion troubleshooting
- Alert acknowledgment workflow
- Disaster recovery procedure (VM rebuild)

#### Task 4.2: Cost Monitoring Setup

Document estimated monthly costs:
- VM (Standard_D2s_v3): ~$70/month
- Storage (1TB + disks): ~$50/month
- Bastion: ~$140/month
- Other services: ~$10/month
- **Total**: ~$270/month (westus3 region)

Set up Azure Cost Management alerts:
- Budget: $300/month
- Alert threshold: 80% ($240)

#### Task 4.3: Security Review

Complete post-deployment security checklist:
- [ ] All resources have diagnostic logging enabled
- [ ] VM has no public IP address
- [ ] Storage account public access disabled
- [ ] Key Vault access restricted to VM managed identity
- [ ] NSG rules follow least-privilege principle
- [ ] Azure Security Center recommendations reviewed
- [ ] No high-severity vulnerabilities identified

#### Task 4.4: Compliance Documentation

Document compliance controls met:
- Infrastructure-as-Code: All resources in version control
- Audit logging: All activity logged to Log Analytics
- Secret management: Passwords in Key Vault, not plaintext
- Network isolation: Private endpoints, no public exposure
- Change management: Deployments require validation gate

### Phase 5: Future Enhancements πŸ”„ OUT OF SCOPE (DOCUMENTED FOR REFERENCE)

Items explicitly out of scope for initial deployment but may be added later:

1. **File Share VM Integration**:
  - Map file share as network drive in VM
  - Configure persistent drive mapping via Group Policy or startup script
  - Document in operational runbooks

2. **Advanced Monitoring**:
  - Custom Log Analytics queries and workbooks
  - Action Groups for email/SMS notifications
  - Integration with external monitoring systems

3. **Backup Configuration**:
  - Azure Backup for VM
  - Azure Files snapshot/backup policies
  - Backup retention policy aligned with compliance requirements

4. **High Availability** (if requirements change):
  - Availability Set or multiple VMs across zones
  - Load Balancer for multi-VM scenarios
  - Azure Site Recovery for disaster recovery

5. **Security Enhancements**:
  - Just-In-Time VM Access
  - Azure Policy assignments
  - Microsoft Defender for Cloud integration
  - Network Watcher flow logs

---

## Risk Assessment & Mitigation

### Technical Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| AVM module breaking changes | Low | Medium | Pin exact module versions, monitor AVM changelogs |
| Bastion deployment timeout | Medium | Low | Allow 20-30 min deployment window, retry if needed |
| Key Vault access issues | Low | High | Thorough RBAC testing, fallback manual password retrieval |
| Private endpoint DNS resolution failures | Low | Medium | Verify Private DNS Zone VNet link, wait for propagation |
| VM extension failures (Windows diagnostics) | Medium | Low | Monitor deployment, acceptable to complete manually |
| Storage account naming conflicts | Low | Low | uniqueString() ensures uniqueness per resource group |

### Operational Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Lost VM password | Low | High | Key Vault provides secure retrieval, document procedure |
| Excessive costs (Bastion always-on) | Medium | Medium | Document monthly costs, set budget alerts at 80% |
| Alert fatigue (too many alerts) | Low | Medium | Start with 3 critical alerts, expand based on operations feedback |
| Insufficient disk space (500GB data disk) | Medium | Low | Alert at 85%, expansion procedure documented |
| Network connectivity issues | Low | High | NAT Gateway provides reliable outbound, private endpoint for inbound |
| Manual Portal changes breaking IaC | Medium | High | Enforce constitution principle I, document prohibition clearly |

### Compliance Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Configuration drift from manual changes | Medium | High | Constitution principle I (IaC-First) prohibits manual changes |
| Audit log gaps | Low | High | All resources send logs to Log Analytics, monitor for gaps |
| Secret exposure (VM password) | Low | Critical | Password generated in Bicep variable, stored only in Key Vault |
| Unauthorized access attempts | Low | High | No public IPs, Bastion-only access, NSG least-privilege rules |
| Non-compliance with retention policies | Low | Medium | Log Analytics retention set to 30+ days, configurable parameter |

---

## Success Criteria

### Deployment Success

- [x] All Bicep templates compile without warnings
- [ ] ARM validation passes successfully
- [ ] What-if analysis shows expected resource creation only
- [ ] Deployment completes in <30 minutes
- [ ] All 20-25 resources created successfully
- [ ] No failed resources or partial deployments

### Functional Success

- [ ] VM accessible via Azure Bastion RDP
- [ ] VM admin password retrievable from Key Vault
- [ ] VM has internet connectivity via NAT Gateway
- [ ] File share accessible from VM via private endpoint
- [ ] Storage account private endpoint resolves to internal IP (10.0.0.128/27 range)
- [ ] All resources logging to Log Analytics
- [ ] 3 metric alerts visible in Azure Portal
- [ ] Test alert fires successfully within 10 minutes

### Compliance Success

- [x] All 6 constitution principles satisfied (see Constitution Check)
- [ ] All resources deployed to westus3 region
- [ ] All resources follow naming convention: {type}-{purpose}-{random}
- [ ] No manual Portal configurations required post-deployment
- [ ] Deployment process documented in quickstart.md
- [ ] All secrets stored in Key Vault, none in version control

### Operational Success

- [ ] Quickstart guide executed successfully by independent tester
- [ ] VM operational for 24 hours with no errors
- [ ] Monitoring dashboards show healthy resource state
- [ ] No high-severity Security Center alerts
- [ ] Total monthly cost projection: $250-$300 USD
- [ ] Documentation sufficient for operational handoff

---

## Appendices

### A. Resource Naming Reference

| Resource Type | Name Pattern | Example |
|---------------|-------------|---------|
| Virtual Machine | `vm-legacyvm-{random}` | `vm-legacyvm-k7m3p` |
| Virtual Network | `vnet-legacyvm-{random}` | `vnet-legacyvm-k7m3p` |
| Subnets | `snet-{purpose}-legacyvm-{random}` | `snet-vm-legacyvm-k7m3p` |
| Network Security Group | `nsg-{purpose}-legacyvm-{random}` | `nsg-vm-legacyvm-k7m3p` |
| NAT Gateway | `nat-legacyvm-{random}` | `nat-legacyvm-k7m3p` |
| Azure Bastion | `bas-legacyvm-{random}` | `bas-legacyvm-k7m3p` |
| Key Vault | `kv-legacyvm-{random}` | `kv-legacyvm-k7m3p` |
| Storage Account | `st{random-no-hyphens}` | `stk7m3p2a` |
| Log Analytics Workspace | `law-legacyvm-{random}` | `law-legacyvm-k7m3p` |
| Metric Alert | `alert-{purpose}-legacyvm-{random}` | `alert-disk-space-legacyvm-k7m3p` |
| Private Endpoint | `pe-{service}-legacyvm-{random}` | `pe-file-legacyvm-k7m3p` |
| Private DNS Zone | `privatelink.{service}.core.windows.net` | `privatelink.file.core.windows.net` |

**Random Suffix**: Generated using `uniqueString(resourceGroup().id)` - produces 13 characters, take first 6: `k7m3p2`

### B. Network Address Allocation

| Component | CIDR | Usable IPs | Purpose |
|-----------|------|------------|---------|
| VNet | 10.0.0.0/24 | 256 | Overall network |
| VM Subnet | 10.0.0.0/27 | 27 usable | Virtual machine NICs |
| Bastion Subnet | 10.0.0.64/26 | 59 usable | Azure Bastion (requires /26) |
| Private Endpoint Subnet | 10.0.0.128/27 | 27 usable | Storage private endpoint |
| Reserved | 10.0.0.160/27 | 27 usable | Future expansion |
| Reserved | 10.0.0.192/26 | 59 usable | Future expansion |

### C. Required Azure Permissions

**Subscription-Level**:
- `Contributor` role OR specific resource provider permissions
- `User Access Administrator` role (for RBAC assignments to Key Vault)

**Resource Providers** (must be registered):
- `Microsoft.Compute`
- `Microsoft.Network`
- `Microsoft.Storage`
- `Microsoft.KeyVault`
- `Microsoft.Insights`
- `Microsoft.OperationalInsights`

### D. Tool Version Requirements

| Tool | Minimum Version | Recommended Version | Check Command |
|------|----------------|---------------------|---------------|
| Azure CLI | 2.65.0 | Latest | `az --version` |
| Bicep CLI | 0.33.0 | Latest | `az bicep version` |
| PowerShell | 7.4 | Latest | `$PSVersionTable.PSVersion` |

### E. Related Documentation

- **Feature Specification**: [spec.md](./spec.md) - Detailed requirements and user stories
- **Module Research**: [research.md](./research.md) - AVM module analysis and alternatives
- **Data Model**: [data-model.md](./data-model.md) - Network topology and resource relationships
- **Deployment Guide**: [quickstart.md](./quickstart.md) - Step-by-step deployment instructions
- **Quality Checklist**: [checklists/requirements.md](./checklists/requirements.md) - Validation results
- **Governance**: [../../.specify/memory/constitution.md](../../.specify/memory/constitution.md) - Project constitution

### F. Change Log

| Date | Version | Author | Changes |
|------|---------|--------|---------|
| 2026-01-27 | 1.0.0 | SpecKit | Initial implementation plan created |

---

**Plan Status**: Phase 1 (Infrastructure Code Implementation) - Ready for bicepconfig.json and main.bicep creation

**Next Command**: `/speckit.tasks` to decompose Phase 1 tasks into granular implementation steps
<!-- markdownlint-disable -->
# Data Model: Legacy VM Workload Infrastructure

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Document network topology, resource relationships, and configuration data model

## Network Topology

### Virtual Network Structure

```
VNet: 10.0.0.0/24 (vnet-legacyvm-{random})
β”‚
β”œβ”€β”€ VM Subnet: 10.0.0.0/27 (snet-vm-legacyvm-{random})
β”‚   β”œβ”€β”€ NAT Gateway attached
β”‚   β”œβ”€β”€ NSG attached (nsg-vm-legacyvm-{random})
β”‚   β”œβ”€β”€ VM Network Interface
β”‚   └── Hosts: Virtual Machine
β”‚
β”œβ”€β”€ Bastion Subnet: 10.0.0.64/26 (AzureBastionSubnet - required name)
β”‚   β”œβ”€β”€ NSG attached (nsg-bastion-legacyvm-{random})
β”‚   └── Hosts: Azure Bastion
β”‚
└── Private Endpoint Subnet: 10.0.0.128/27 (snet-pe-legacyvm-{random})
    β”œβ”€β”€ NSG attached (nsg-pe-legacyvm-{random})
    └── Hosts: Storage Account Private Endpoint
```

### Address Space Allocation

| Resource | CIDR | Usable IPs | Purpose |
|----------|------|------------|---------|
| VNet | 10.0.0.0/24 | 256 | Overall network |
| VM Subnet | 10.0.0.0/27 | 32 (27 usable) | Virtual machine network interfaces |
| Bastion Subnet | 10.0.0.64/26 | 64 (59 usable) | Azure Bastion (requires /26 minimum) |
| Private Endpoint Subnet | 10.0.0.128/27 | 32 (27 usable) | Storage account private endpoints |
| Reserved | 10.0.0.160/27 | 32 | Future expansion |
| Reserved | 10.0.0.192/26 | 64 | Future expansion |

## Resource Dependency Graph

```
Resource Group
β”‚
β”œβ”€β”€ Log Analytics Workspace
β”‚   └── (Used by all diagnostic settings)
β”‚
β”œβ”€β”€ Virtual Network
β”‚   β”œβ”€β”€ Depends on: None
β”‚   └── Used by: Bastion, VM NIC, Private Endpoint
β”‚
β”œβ”€β”€ Network Security Groups (3)
β”‚   β”œβ”€β”€ nsg-vm-legacyvm-{random}
β”‚   β”œβ”€β”€ nsg-bastion-legacyvm-{random}
β”‚   └── nsg-pe-legacyvm-{random}
β”‚   β”œβ”€β”€ Depends on: VNet (for subnet association)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ NAT Gateway
β”‚   β”œβ”€β”€ Public IP (auto-created)
β”‚   β”œβ”€β”€ Depends on: None
β”‚   β”œβ”€β”€ Associated with: VM Subnet
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Azure Bastion
β”‚   β”œβ”€β”€ Public IP (auto-created)
β”‚   β”œβ”€β”€ Depends on: VNet (Bastion subnet)
β”‚   β”œβ”€β”€ Depends on: NSG (bastion subnet)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Key Vault
β”‚   β”œβ”€β”€ Depends on: None (deployed early)
β”‚   β”œβ”€β”€ Secret: VM admin password (generated)
β”‚   β”œβ”€β”€ RBAC: VM managed identity (Key Vault Secrets User)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Private DNS Zone
β”‚   β”œβ”€β”€ Name: privatelink.file.core.windows.net
β”‚   β”œβ”€β”€ VNet Link: Main VNet
β”‚   └── Depends on: VNet
β”‚
β”œβ”€β”€ Storage Account
β”‚   β”œβ”€β”€ File Share (1024 GiB)
β”‚   β”œβ”€β”€ Depends on: None
β”‚   β”œβ”€β”€ Public access: Disabled
β”‚   β”œβ”€β”€ Diagnostic settings β†’ Log Analytics
β”‚   └── Private Endpoint
β”‚       β”œβ”€β”€ Depends on: Storage Account, VNet, Private DNS Zone
β”‚       β”œβ”€β”€ Subnet: Private Endpoint Subnet
β”‚       └── DNS integration: Private DNS Zone
β”‚
└── Virtual Machine
    β”œβ”€β”€ Depends on: VNet, Key Vault (for password)
    β”œβ”€β”€ Managed Identity: System-assigned
    β”œβ”€β”€ OS Disk: Standard HDD
    β”œβ”€β”€ Data Disk: 500GB Standard HDD
    β”œβ”€β”€ Network Interface
    β”‚   β”œβ”€β”€ Depends on: VM Subnet
    β”‚   └── No Public IP
    β”œβ”€β”€ Password: Retrieved from Key Vault secret
    └── Diagnostic settings β†’ Log Analytics

β”œβ”€β”€ Azure Monitor Alerts (3)
    β”œβ”€β”€ VM Stopped Alert
    β”‚   └── Depends on: VM
    β”œβ”€β”€ Disk Space Alert
    β”‚   └── Depends on: VM
    └── Key Vault Access Failures Alert
        └── Depends on: Key Vault
```

## Resource Configuration Model

### Virtual Machine
```yaml
Name Pattern: vm-legacyvm-{random}
Computer Name: vm-{random} (≀15 chars total)
Configuration:
  Size: Standard_D2s_v3
  OS: Windows Server 2016
  OS Disk:
    Type: Standard_LRS (HDD performance tier)
    Size: Default (127 GB or OS default)
  Data Disks:
    - Name: datadisk-01
      Size: 500 GB
      Type: Standard_LRS (HDD performance tier)
      LUN: 0
  Admin:
    Username: vmadmin
    Password: {From Key Vault secret}
  Identity:
    Type: SystemAssigned
  Zone: {Parameter: 1, 2, or 3}
  Network:
    NIC:
      Subnet: VM Subnet
      Public IP: None
      Private IP: Dynamic
  Diagnostics:
    Boot Diagnostics: Enabled (Managed)
    Guest Diagnostics: Windows (via Log Analytics agent)
```

### Key Vault
```yaml
Name Pattern: kv-legacyvm-{random}
Configuration:
  SKU: Standard
  Access Model: RBAC (Azure role-based access control)
  Public Network Access: Enabled (simplified for legacy workload)
  Soft Delete: Enabled (90 days)
  Purge Protection: Disabled (not required for legacy workload)
  Secrets:
    - Name: {Parameter: vmAdminPasswordSecretName}
      Value: {Generated: uniqueString-based password}
      Content Type: text/plain
  RBAC Assignments:
    - Principal: VM Managed Identity
      Role: Key Vault Secrets User
      Scope: Key Vault
  Diagnostics:
    Logs: All categories
    Metrics: All metrics
    Destination: Log Analytics
```

### Storage Account
```yaml
Name Pattern: st{random-no-hyphens} (≀24 chars)
Configuration:
  Kind: StorageV2
  SKU: Standard_LRS (HDD-based)
  Access Tier: Hot
  Public Network Access: Disabled
  Minimum TLS: 1.2
  File Services:
    Shares:
      - Name: fileshare
        Quota: 1024 GiB
        Access Tier: TransactionOptimized
  Private Endpoints:
    - Service: file
      Subnet: Private Endpoint Subnet
      DNS Integration: privatelink.file.core.windows.net
  Diagnostics:
    Logs: All categories (StorageRead, StorageWrite, StorageDelete)
    Metrics: All metrics
    Destination: Log Analytics
```

### Network Security Groups

#### VM Subnet NSG
```yaml
Name: nsg-vm-legacyvm-{random}
Security Rules:
  Inbound:
    - Name: DenyAllInbound
      Priority: 4096
      Direction: Inbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
  Outbound:
    - Name: AllowInternetOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: Internet
      SourcePort: *
      DestinationPort: *
    - Name: AllowVnetOutbound
      Priority: 200
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: *
    - Name: DenyAllOutbound
      Priority: 4096
      Direction: Outbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
```

#### Bastion Subnet NSG
```yaml
Name: nsg-bastion-legacyvm-{random}
Security Rules:
  # Standard Azure Bastion required rules
  Inbound:
    - Name: AllowHttpsInbound
      Priority: 100
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: Internet
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowGatewayManagerInbound
      Priority: 110
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: GatewayManager
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowAzureLoadBalancerInbound
      Priority: 120
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: AzureLoadBalancer
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowBastionHostCommunication
      Priority: 130
      Direction: Inbound
      Access: Allow
      Protocol: *
      Source: VirtualNetwork
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 8080,5701
  Outbound:
    - Name: AllowSshRdpOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 22,3389
    - Name: AllowAzureCloudOutbound
      Priority: 110
      Direction: Outbound
      Access: Allow
      Protocol: Tcp
      Source: *
      Destination: AzureCloud
      SourcePort: *
      DestinationPort: 443
    - Name: AllowBastionCommunication
      Priority: 120
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: VirtualNetwork
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 8080,5701
    - Name: AllowGetSessionInformation
      Priority: 130
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: Internet
      SourcePort: *
      DestinationPort: 80
```

#### Private Endpoint Subnet NSG
```yaml
Name: nsg-pe-legacyvm-{random}
Security Rules:
  Inbound:
    - Name: AllowVMSubnetInbound
      Priority: 100
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: 10.0.0.0/27
      Destination: *
      SourcePort: *
      DestinationPort: 445
    - Name: DenyAllInbound
      Priority: 4096
      Direction: Inbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
  Outbound:
    - Name: AllowAllOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
```

### Azure Monitor Alerts

#### Alert 1: VM Stopped/Deallocated
```yaml
Name: alert-vm-stopped-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Virtual Machine
  Metric:
    Namespace: Microsoft.Compute/virtualMachines
    Name: Percentage CPU
  Condition:
    Operator: LessThan
    Threshold: 1
    Aggregation: Average
    Window: 15 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: VM appears to be stopped or deallocated"
```

#### Alert 2: Disk Space Exceeded
```yaml
Name: alert-disk-space-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Virtual Machine
  Metric:
    Namespace: Microsoft.Compute/virtualMachines
    Name: OS Disk Used Percentage
  Condition:
    Operator: GreaterThan
    Threshold: 85
    Aggregation: Average
    Window: 5 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: Disk space exceeded 85% threshold"
```

#### Alert 3: Key Vault Access Failures
```yaml
Name: alert-kv-access-fail-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Key Vault
  Metric:
    Namespace: Microsoft.KeyVault/vaults
    Name: ServiceApiHit
  Filter:
    Dimension: ActivityName
    Values: SecretGet
    Result: Failed
  Condition:
    Operator: GreaterThan
    Threshold: 0
    Aggregation: Count
    Window: 5 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: Key Vault secret access failures detected"
```

## Deployment Sequence

Based on ARM dependency analysis, resources will deploy in this approximate order:

1. **Phase 1: Foundation** (Parallel)
  - Log Analytics Workspace
  - Virtual Network (with subnets)
  - Network Security Groups

2. **Phase 2: Network & Security** (Depends on Phase 1)
  - NAT Gateway (associates with VM subnet)
  - Azure Bastion (requires subnet and NSG)
  - Private DNS Zone (requires VNet)
  - Key Vault (generates and stores password)

3. **Phase 3: Storage** (Depends on Phase 2)
  - Storage Account (with file share)
  - Private Endpoint (requires storage account, VNet, DNS zone)

4. **Phase 4: Compute** (Depends on Phases 1-3)
  - Virtual Machine (requires VNet, Key Vault secret, zone assignment)

5. **Phase 5: Monitoring** (Depends on Phase 4)
  - Azure Monitor Alerts (require VM and Key Vault to be deployed)

## Parameter Data Model

```yaml
# Required Parameters
parameters:
  resourceGroupName: string
    description: Name of the resource group for deployment
    example: rg-legacyvm-prod

  location: string
    description: Azure region for deployment
    default: westus3
    validation: Must be valid Azure region

  vmSize: string
    description: Virtual machine size
    default: Standard_D2s_v3
    validation: Must support Windows Server 2016

  vmAdminUsername: string
    description: VM administrator username
    default: vmadmin
    minLength: 1
    maxLength: 20

  vmAdminPasswordSecretName: string
    description: Name of Key Vault secret for VM admin password
    default: vm-admin-password
    minLength: 1
    maxLength: 127

  availabilityZone: int
    description: Availability zone for zone-capable resources
    allowed: [1, 2, 3]
    default: 1

  fileShareQuotaGiB: int
    description: File share quota in GiB
    default: 1024
    minValue: 100
    maxValue: 102400

  logAnalyticsRetentionDays: int
    description: Log Analytics data retention in days
    default: 30
    minValue: 30
    maxValue: 730

# Generated Values (not parameters)
variables:
  randomSuffix: uniqueString(resourceGroup().id)
  vmPassword: P@ssw0rd!{uniqueString(resourceGroup().id, deployment().name)}

  # Resource Names
  vnetName: vnet-legacyvm-{randomSuffix}
  vmName: vm-legacyvm-{randomSuffix}
  storageAccountName: st{replace(randomSuffix, '-', '')}
  keyVaultName: kv-legacyvm-{randomSuffix}
  logAnalyticsName: law-legacyvm-{randomSuffix}
```

## Tags Model

All resources will be tagged with:

```yaml
tags:
  workload: legacy-vm
  environment: production
  compliance: legacy-retention
  managedBy: bicep-avm
  deploymentDate: {deployment().timestamp}
```

## Security Model

### RBAC Assignments

| Principal | Role | Scope | Purpose |
|-----------|------|-------|---------|
| VM Managed Identity | Key Vault Secrets User | Key Vault | Read VM admin password |
| VM Managed Identity | Storage Blob Data Contributor | Storage Account | Access file share (future) |

### Network Security

| Source | Destination | Protocol/Port | Action | Purpose |
|--------|-------------|---------------|--------|---------|
| Internet | Bastion (443) | TCP/443 | Allow | Admin RDP access |
| Bastion | VM (3389) | TCP/3389 | Allow | RDP to VM |
| VM | Internet | Any | Allow | Outbound via NAT Gateway |
| VM | Storage PE (445) | TCP/445 | Allow | File share access |
| Any | VM | Any | Deny | No direct access to VM |

## Monitoring Data Model

### Diagnostic Settings Targets

All resources with diagnostic settings send to:
- **Primary**: Log Analytics Workspace
- **Categories**: All available log categories
- **Metrics**: All available metrics

### Alert Notification Model

- **Channel**: Azure Portal only
- **No Action Groups**: Alerts visible in portal alerts blade
- **Severity**: All set to Critical (Sev 0)
- **Auto-Mitigation**: Disabled (require manual acknowledgment)
<!-- markdownlint-disable -->
# Research: Legacy VM Workload AVM Modules

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Research and document AVM module selections, versions, and configuration approaches

## AVM Module Inventory

### Primary Infrastructure Modules

#### 1. Virtual Network
- **Module**: `avm/res/network/virtual-network`
- **Latest Version**: 0.7.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/virtual-network/0.7.2/avm/res/network/virtual-network/README.md
- **Decision**: Use this module for VNet and subnet deployment
- **Rationale**: Official AVM module with built-in support for subnets, NSG assignments, NAT gateway association, and diagnostic settings
- **Key Parameters Needed**:
  - Address space: 10.0.0.0/24
  - Subnets: VM (10.0.0.0/27), Bastion (10.0.0.64/26), Private endpoint (10.0.0.128/27)
  - NSG associations per subnet
  - NAT gateway assignment to VM subnet

#### 2. Virtual Machine
- **Module**: `avm/res/compute/virtual-machine`
- **Latest Version**: 0.21.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/compute/virtual-machine/0.21.0/avm/res/compute/virtual-machine/README.md
- **Decision**: Use this module for VM deployment
- **Rationale**: Comprehensive AVM module with built-in support for managed disks, managed identity, diagnostic settings, and guest configuration
- **Key Parameters Needed**:
  - VM size: Standard_D2s_v3
  - OS: Windows Server 2016
  - Computer name: ≀15 characters
  - Admin username: vmadmin
  - Admin password: Reference to Key Vault secret
  - Managed identity: System-assigned
  - Data disks: 500GB HDD
  - Availability zone: 1-3 (parameter-driven)
  - No public IP

#### 3. Azure Bastion
- **Module**: `avm/res/network/bastion-host`
- **Latest Version**: 0.8.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/bastion-host/0.8.2/avm/res/network/bastion-host/README.md
- **Decision**: Use this module for bastion deployment
- **Rationale**: AVM module with built-in diagnostic settings and public IP creation
- **Key Parameters Needed**:
  - Subnet: Bastion subnet (10.0.0.64/26)
  - SKU: Basic (cost-effective for legacy workload)
  - Diagnostic settings to Log Analytics

#### 4. Storage Account
- **Module**: `avm/res/storage/storage-account`
- **Latest Version**: 0.31.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/storage/storage-account/0.31.0/avm/res/storage/storage-account/README.md
- **Decision**: Use this module for storage account and file share
- **Rationale**: Comprehensive AVM module with built-in file share, private endpoint, diagnostic settings, and network rules support
- **Key Parameters Needed**:
  - SKU: Standard_LRS (HDD-based)
  - File share quota: 1024 GiB
  - Private endpoint enabled
  - Public network access disabled
  - Diagnostic settings to Log Analytics

#### 5. NAT Gateway
- **Module**: `avm/res/network/nat-gateway`
- **Latest Version**: 2.0.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/nat-gateway/2.0.1/avm/res/network/nat-gateway/README.md
- **Decision**: Use this module for NAT gateway
- **Rationale**: AVM module with public IP creation and zone support
- **Key Parameters Needed**:
  - Zone: parameter-driven (1-3)
  - Public IP: Auto-created by module

#### 6. Network Security Group
- **Module**: `avm/res/network/network-security-group`
- **Latest Version**: 0.5.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/network-security-group/0.5.2/avm/res/network/network-security-group/README.md
- **Decision**: Use this module for all three NSGs (VM subnet, bastion subnet, private endpoint subnet)
- **Rationale**: AVM module with built-in diagnostic settings and security rule definitions
- **Key Parameters Needed**:
  - VM subnet NSG: Allow outbound to internet via NAT gateway, deny other traffic
  - Bastion subnet NSG: Standard bastion rules (inbound 443, outbound to VM subnet)
  - Private endpoint subnet NSG: Allow traffic from VM subnet only

#### 7. Key Vault
- **Module**: `avm/res/key-vault/vault`
- **Latest Version**: 0.13.3
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/key-vault/vault/0.13.3/avm/res/key-vault/vault/README.md
- **Decision**: Use this module for Key Vault and secret storage
- **Rationale**: AVM module with built-in secret creation using `secrets` parameter array, RBAC support, diagnostic settings, and network rules
- **Key Features**:
  - Supports `secrets` parameter for creating secrets at deployment time
  - Can generate password using `uniqueString()` and store in secret
  - Built-in RBAC assignments
  - Private endpoint support (optional for this scenario)
  - Diagnostic settings interface
- **Password Generation Approach**:
  - Use Bicep `uniqueString()` function to generate complex password
  - Combine multiple seed values for randomness
  - Store in Key Vault secret via module's `secrets` parameter
  - Reference secret in VM module

#### 8. Log Analytics Workspace
- **Module**: `avm/res/operational-insights/workspace`
- **Latest Version**: 0.15.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/operational-insights/workspace/0.15.0/avm/res/operational-insights/workspace/README.md
- **Decision**: Use this module for Log Analytics
- **Rationale**: AVM module with retention configuration and solution deployment support
- **Key Parameters Needed**:
  - Retention days: 30 (default assumption)
  - SKU: PerGB2018

#### 9. Private Endpoint
- **Module**: `avm/res/network/private-endpoint`
- **Latest Version**: 0.11.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/private-endpoint/0.11.1/avm/res/network/private-endpoint/README.md
- **Decision**: Use this module for storage account file share private endpoint
- **Rationale**: AVM module with built-in private DNS zone group configuration
- **Key Parameters Needed**:
  - Service connection: Storage account file service
  - Subnet: Private endpoint subnet
  - Private DNS zone: privatelink.file.core.windows.net (manual creation)

#### 10. Azure Monitor Alerts
- **Module**: `avm/res/insights/metric-alert`
- **Latest Version**: 0.4.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/insights/metric-alert/0.4.1/avm/res/insights/metric-alert/README.md
- **Decision**: Use this module for all three critical alerts
- **Rationale**: AVM module supporting metric-based alerts for VM and Key Vault
- **Alerts to Create**:
  1. VM stopped/deallocated
  2. Disk space > 85%
  3. Key Vault access failures
- **Note**: Portal-only notifications (no action groups needed for this scenario)

### Supporting Modules

#### 11. Managed Disk
- **Included in VM Module**: The Virtual Machine module handles data disk creation inline
- **No separate module needed**: Data disks are specified as parameters to the VM module

#### 12. Private DNS Zone
- **Module**: `avm/res/network/private-dns-zone`
- **Latest Version**: 0.8.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/private-dns-zone/0.8.0/avm/res/network/private-dns-zone/README.md
- **Decision**: Use this module for private DNS zone for file share private endpoint
- **Rationale**: Required for DNS resolution of storage account file share through private endpoint
- **Key Parameters Needed**:
  - Zone name: privatelink.file.core.windows.net
  - VNet link to main VNet

## Alternative Approaches Considered

### Alternative 1: Direct Resource Declarations
- **Approach**: Use direct Bicep resource declarations instead of AVM modules
- **Rejected**: Violates constitution principle II (AVM-Only Modules)
- **Trade-offs**: Would provide more control but lose benefits of tested, maintained, secure-by-default configurations

### Alternative 2: Pattern Module for VM Workloads
- **Approach**: Search for existing AVM pattern module combining VM, networking, and storage
- **Evaluated**: No suitable pattern module exists for this specific legacy VM scenario
- **Decision**: Compose solution from resource modules per constitution

### Alternative 3: Deployment Scripts for Password Generation
- **Approach**: Use Azure Deployment Scripts to generate and store VM password
- **Rejected**: User requirement specifies using uniqueString() and avoiding external helper scripts
- **Decision**: Generate password inline using Bicep uniqueString() function and store via Key Vault module's secrets parameter

### Alternative 4: Azure Backup Integration
- **Approach**: Include Azure Backup configuration for VM
- **Rejected**: Explicitly out of scope per specification
- **Note**: Can be added later if requirements change

## Bicep Language Features Required

### Password Generation Pattern
```bicep
// Generate complex password using uniqueString with multiple seeds
var generatedPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, utcNow('u'))}'
```

### Resource Dependency Management
- Let ARM manage dependencies automatically
- Explicit `dependsOn` only when implicit dependency isn't detected
- Use resource symbolic names for references

### Parameter Validation
- Use decorators: `@minLength()`, `@maxLength()`, `@allowed()`
- Validate VM computer name length (≀15 chars)
- Validate storage account name (≀24 chars, lowercase, alphanumeric)

## Key Configuration Decisions

### Resource Naming
- **Pattern**: {resourceType}-{purpose}-{randomSuffix}
- **Random Suffix**: Use uniqueString() with 6 characters
- **Examples**:
  - VNet: `vnet-legacyvm-${uniqueString(resourceGroup().id)}`
  - VM: `vm-legacyvm-${uniqueString(resourceGroup().id)}`
  - Storage: `st${replace(uniqueString(resourceGroup().id), '-', '')}` (no hyphens, ≀24 chars)
  - Key Vault: `kv-legacyvm-${uniqueString(resourceGroup().id)}`

### Network Security
- **VM Subnet NSG**: Allow outbound internet (via NAT), deny all inbound except from bastion
- **Bastion Subnet NSG**: Follow Azure Bastion NSG requirements
- **Private Endpoint Subnet NSG**: Allow inbound from VM subnet on port 445 (SMB)

### Diagnostic Settings
- **Target**: Log Analytics Workspace (centralized)
- **Resources to Monitor**: VM, Key Vault, Storage Account, NSGs, Bastion
- **Log Categories**: All available categories
- **Metrics**: All available metrics

### Availability Zones
- **VM**: Deploy to zone specified by parameter (1, 2, or 3)
- **NAT Gateway**: Deploy to same zone as VM
- **Managed Disks**: Automatically zone-aligned with VM

## Implementation Notes

### Single Template Approach
- All resources in main.bicep
- No nested modules or separate Bicep files
- ARM dependency management handles deployment order

### Parameter Management
- All configurable values in main.bicepparam
- Rich comments explaining each parameter
- Default values where appropriate
- No hardcoded values in template

### Bicep CLI Version
- **Minimum**: Latest stable version (0.33.0 or higher at time of writing)
- **Recommendation**: Always use latest for newest AVM module support
- **Verification**: Run `bicep --version` before deployment

### Module Version Pinning
- **Required**: Always pin to specific versions (never 'latest' tag)
- **Format**: `br/public:avm/res/network/virtual-network:0.7.2`
- **Maintenance**: Update versions explicitly when needed

## Open Questions Resolved

1. **How to generate VM password without external scripts?**
  - **Resolution**: Use Bicep `uniqueString()` function with multiple seeds
  - **Implementation**: Store generated password in Key Vault using module's `secrets` parameter

2. **How to connect file share to VM?**
  - **Resolution**: Out of scope for initial deployment per user guidance
  - **Future**: Will require VM extension or post-deployment script

3. **Should we use private endpoint for Key Vault?**
  - **Resolution**: No, not required for this legacy workload
  - **Justification**: Adds complexity without clear benefit for single VM scenario

4. **What alert notification channels?**
  - **Resolution**: Portal notifications only (clarified during specification)
  - **Implementation**: Create metric alerts without action groups

5. **Module version for optimal features?**
  - **Resolution**: Always use latest stable version listed in AVM metadata
  - **Verification**: Confirmed all required features available in latest versions

## Next Steps

1. Create data-model.md with network topology and resource relationships
2. Write deployment quickstart guide
3. Fill implementation plan template
4. Create bicepconfig.json with module version analyzer
<!-- markdownlint-disable -->
# Quickstart: Deploy Legacy VM Workload

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Step-by-step deployment guide with validation and troubleshooting

## Prerequisites

### Required Tools

1. **Azure CLI** (v2.65.0 or later)
  ```powershell
  # Check version
  az --version

  # Install/upgrade if needed
  # Windows: Download from https://aka.ms/installazurecliwindows
  # Or use winget
  winget install -e --id Microsoft.AzureCLI
  ```

2. **Bicep CLI** (v0.33.0 or later)
  ```powershell
  # Check version
  az bicep version

  # Install/upgrade
  az bicep install
  az bicep upgrade
  ```

3. **PowerShell** (v7.4 or later recommended)
  ```powershell
  # Check version
  $PSVersionTable.PSVersion

  # Install if needed
  winget install --id Microsoft.Powershell --source winget
  ```

### Azure Permissions

You need the following permissions on the target subscription:

- **Owner** or **Contributor** role at subscription or resource group level
- **User Access Administrator** role (if deploying RBAC assignments)
- Permissions to create resources in **westus3** region

### Authentication

```powershell
# Login to Azure
az login

# Set the target subscription
az account set --subscription "<subscription-id-or-name>"

# Verify current context
az account show --output table
```

## Repository Structure

```
avm-workload/
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ main.bicep            # Main infrastructure template
β”‚   β”œβ”€β”€ main.bicepparam       # Deployment parameters
β”‚   └── bicepconfig.json      # Bicep configuration
β”œβ”€β”€ specs/
β”‚   └── 001-legacy-vm-workload/
β”‚       β”œβ”€β”€ spec.md           # Feature specification
β”‚       β”œβ”€β”€ plan.md           # Implementation plan
β”‚       β”œβ”€β”€ data-model.md     # Architecture documentation
β”‚       └── quickstart.md     # This file
└── .specify/
    └── memory/
        └── constitution.md   # Governance framework
```

## Deployment Workflow

### Step 1: Review Parameters

Edit `infra/main.bicepparam` to customize deployment:

```bicep
using './main.bicep'

// Required parameters
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param availabilityZone = 1
param fileShareQuotaGiB = 1024
param logAnalyticsRetentionDays = 30

// Optional: Override resource names
// param vmName = 'vm-custom-name'
// param vnetName = 'vnet-custom-name'
```

**Key Parameters**:
- `vmSize`: Virtual machine SKU (must support Windows Server 2016)
- `vmAdminUsername`: Administrator username for the VM
- `availabilityZone`: Availability zone (1, 2, or 3)
- `fileShareQuotaGiB`: Storage file share quota (default 1024 GiB)
- `logAnalyticsRetentionDays`: Log retention period (30-730 days)

### Step 2: Pre-Deployment Validation

#### 2.1 Bicep Compilation

Verify the template compiles without errors:

```powershell
# Navigate to infrastructure directory
cd C:\SOURCE\avm-workload\infra

# Build Bicep template
bicep build main.bicep

# Check for warnings
# Fix any warnings reported by the analyzer
```

**Expected Output**: `main.json` file created with no errors or warnings.

#### 2.2 Template Validation

Validate deployment against Azure:

```powershell
# Create resource group (if it doesn't exist)
az group create `
  --name rg-legacyvm-prod `
  --location westus3

# Validate deployment
az deployment group validate `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Check validation result
if ($LASTEXITCODE -eq 0) {
    Write-Host "βœ… Validation passed" -ForegroundColor Green
} else {
    Write-Host "❌ Validation failed - review errors above" -ForegroundColor Red
    exit 1
}
```

**Expected Output**: `provisioningState: Succeeded`

#### 2.3 What-If Analysis

Preview what resources will be created:

```powershell
# Run what-if analysis
az deployment group what-if `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Review output:
# - Green (+): Resources to be created
# - Yellow (~): Resources to be modified
# - Red (x): Resources to be deleted
# - White (=): No change
```

**Review Checklist**:
- [ ] 1 Virtual Network with 3 subnets
- [ ] 3 Network Security Groups
- [ ] 1 NAT Gateway with Public IP
- [ ] 1 Azure Bastion with Public IP
- [ ] 1 Key Vault with 1 secret
- [ ] 1 Storage Account with 1 file share
- [ ] 1 Private Endpoint
- [ ] 1 Private DNS Zone with VNet link
- [ ] 1 Virtual Machine with NIC, OS disk, data disk
- [ ] 1 Log Analytics Workspace
- [ ] 3 Metric Alerts
- [ ] Multiple diagnostic settings
- [ ] RBAC role assignments

**STOP**: Do not proceed if what-if shows unexpected resource deletions or modifications.

### Step 3: Deploy Infrastructure

#### 3.1 Execute Deployment

```powershell
# Deploy infrastructure
az deployment group create `
  --name "legacyvm-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Deployment typically takes 15-20 minutes
# Monitor progress in Azure Portal: Resource Groups > rg-legacyvm-prod > Deployments
```

**Expected Duration**: 15-20 minutes

**Deployment Phases**:
1. **0-2 min**: Log Analytics, VNet, NSGs
2. **2-8 min**: NAT Gateway, Bastion, Private DNS Zone, Key Vault
3. **8-12 min**: Storage Account, Private Endpoint
4. **12-18 min**: Virtual Machine (longest phase)
5. **18-20 min**: Monitor Alerts

#### 3.2 Monitor Deployment

**Option A: Azure CLI**
```powershell
# Watch deployment status
az deployment group show `
  --name "legacyvm-<timestamp>" `
  --resource-group rg-legacyvm-prod `
  --query "{State:properties.provisioningState, Duration:properties.duration}" `
  --output table
```

**Option B: Azure Portal**
1. Navigate to: [Azure Portal](https://portal.azure.com)
2. Go to: **Resource Groups** > **rg-legacyvm-prod** > **Deployments**
3. Click on the active deployment to see detailed progress
4. Monitor each resource deployment status

### Step 4: Post-Deployment Verification

#### 4.1 Verify Resources

```powershell
# List all resources in the resource group
az resource list `
  --resource-group rg-legacyvm-prod `
  --output table

# Expected count: 20-25 resources
# Key resources to verify:
# - Virtual Machine
# - Virtual Network
# - Storage Account
# - Key Vault
# - Azure Bastion
# - Log Analytics Workspace
```

#### 4.2 Test Bastion Connectivity

**Via Azure Portal**:
1. Go to: **Virtual Machines** > **vm-legacyvm-{random}**
2. Click: **Connect** > **Bastion**
3. Enter credentials:
  - **Username**: `vmadmin`
  - **Password**: Get from Key Vault (see below)
4. Click: **Connect**

**Retrieve VM Password**:
```powershell
# Get Key Vault name
$kvName = az keyvault list `
  --resource-group rg-legacyvm-prod `
  --query "[0].name" `
  --output tsv

# Get VM admin password from Key Vault
az keyvault secret show `
  --name vm-admin-password `
  --vault-name $kvName `
  --query "value" `
  --output tsv
```

**Expected Result**: Successful RDP connection to Windows Server 2016 VM.

#### 4.3 Verify Logs in Log Analytics

```powershell
# Get Log Analytics workspace ID
$workspaceId = az monitor log-analytics workspace show `
  --resource-group rg-legacyvm-prod `
  --workspace-name law-legacyvm-{random} `
  --query "customerId" `
  --output tsv

Write-Host "Log Analytics Workspace ID: $workspaceId"
Write-Host "Portal: https://portal.azure.com#blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F{subscription-id}%2FresourceGroups%2Frg-legacyvm-prod%2Fproviders%2FMicrosoft.OperationalInsights%2Fworkspaces%2Flaw-legacyvm-{random}"
```

**Via Azure Portal**:
1. Go to: **Log Analytics Workspaces** > **law-legacyvm-{random}**
2. Click: **Logs**
3. Run query to verify diagnostic logs:

```kusto
// Query 1: Verify VM activity logs
AzureActivity
| where ResourceGroup == "rg-legacyvm-prod"
| where ResourceType == "Microsoft.Compute/virtualMachines"
| summarize count() by OperationName
| order by count_ desc

// Query 2: Verify Key Vault audit logs
AzureDiagnostics
| where ResourceType == "VAULTS"
| where ResourceGroup == "rg-legacyvm-prod"
| summarize count() by OperationName, ResultType
| order by count_ desc

// Query 3: Verify Storage Account logs
StorageFileLogs
| where AccountName startswith "st"
| summarize count() by OperationName
| order by count_ desc

// Query 4: Check for any errors
AzureDiagnostics
| where ResourceGroup == "rg-legacyvm-prod"
| where Level == "Error"
| project TimeGenerated, ResourceType, OperationName, ResultDescription
| order by TimeGenerated desc
```

**Expected Results**: Logs appearing for all resources within 5-10 minutes of deployment.

#### 4.4 Test Alerts

**Test 1: Disk Space Alert** (Optional - requires VM modification)
```powershell
# WARNING: This will consume disk space on the VM
# Only run if you want to test alert firing

# Connect to VM via Bastion, then run in VM:
# fsutil file createnew C:\testfile.tmp 100000000000  # 100GB file

# Wait 5-10 minutes for alert to fire
# Check: Azure Portal > Monitor > Alerts
```

**Test 2: Key Vault Access Failure** (Safe test)
```powershell
# Attempt to get a non-existent secret (should generate access failure log)
az keyvault secret show `
  --name "non-existent-secret" `
  --vault-name $kvName 2>$null

# Wait 5 minutes, then check:
# Azure Portal > Monitor > Alerts > alert-kv-access-fail-legacyvm-{random}
```

**Expected Behavior**: Alerts visible in Azure Portal within 5-10 minutes of trigger condition.

#### 4.5 Verify Network Connectivity

**From VM (via Bastion RDP session)**:

```powershell
# Test internet connectivity via NAT Gateway
Test-NetConnection -ComputerName google.com -Port 443

# Test Azure DNS resolution
nslookup st{random}.file.core.windows.net

# Verify private endpoint resolution
nslookup st{random}.privatelink.file.core.windows.net

# Expected: Private IP from 10.0.0.128/27 range

# Test file share access (future - after mapping)
# net use Z: \\st{random}.file.core.windows.net\fileshare
```

**Expected Results**:
- Internet access works (NAT Gateway)
- Private endpoint resolves to internal IP (10.0.0.128/27)
- File share accessible from VM

## Troubleshooting

### Issue: Bicep Build Fails

**Symptoms**: `bicep build` reports errors or warnings

**Solutions**:
1. **Check Bicep CLI version**:
  ```powershell
  az bicep version
  # Should be v0.33.0 or later
  az bicep upgrade
  ```

2. **Review analyzer warnings**:
  - Open `main.bicep` in VS Code with Bicep extension
  - Fix any red/yellow squiggles
  - Common issues: outdated module versions, missing required parameters

3. **Validate bicepconfig.json**:
  ```powershell
  # Ensure file exists and is valid JSON
  Get-Content infra/bicepconfig.json | ConvertFrom-Json
  ```

### Issue: Validation Fails

**Symptoms**: `az deployment group validate` returns errors

**Common Errors**:

1. **"Resource provider not registered"**
  ```powershell
  # Register required providers
  az provider register --namespace Microsoft.Compute
  az provider register --namespace Microsoft.Network
  az provider register --namespace Microsoft.Storage
  az provider register --namespace Microsoft.KeyVault
  az provider register --namespace Microsoft.Insights

  # Wait for registration to complete (2-5 minutes)
  az provider show --namespace Microsoft.Compute --query "registrationState"
  ```

2. **"Quota exceeded"**
  - Check Azure subscription quotas
  - Request quota increase if needed: Portal > Subscriptions > Usage + quotas

3. **"Invalid parameter value"**
  - Review `main.bicepparam` for typos
  - Ensure `vmSize` is valid for westus3 region
  - Verify availability zone is 1, 2, or 3

### Issue: Deployment Hangs or Times Out

**Symptoms**: Deployment runs longer than 30 minutes

**Diagnosis**:
```powershell
# Check deployment status
az deployment group show `
  --name "legacyvm-<timestamp>" `
  --resource-group rg-legacyvm-prod `
  --query "properties.{State:provisioningState, SubState:provisioningDetails}" `
  --output json

# View deployment operations
az deployment operation group list `
  --resource-group rg-legacyvm-prod `
  --name "legacyvm-<timestamp>" `
  --query "[?properties.provisioningState=='Failed' || properties.provisioningState=='Running']" `
  --output table
```

**Solutions**:
1. **VM creation timeout**: May indicate VM extension failures
  - Check: Portal > VM > Extensions and applications
  - Solution: Redeploy with `--no-wait` flag, monitor separately

2. **Bastion timeout**: Check Public IP allocation
  - Verify Public IP quota not exceeded
  - Check NSG rules on Bastion subnet

3. **Private Endpoint timeout**: DNS propagation delay
  - Wait additional 5-10 minutes
  - Verify Private DNS Zone linked to VNet

### Issue: VM Password Not Working

**Symptoms**: Cannot connect to VM via Bastion with retrieved password

**Solutions**:
1. **Re-retrieve password from Key Vault**:
  ```powershell
  $kvName = az keyvault list --resource-group rg-legacyvm-prod --query "[0].name" -o tsv
  $password = az keyvault secret show --name vm-admin-password --vault-name $kvName --query "value" -o tsv
  Write-Host "Password: $password"
  ```

2. **Check Key Vault access**:
  ```powershell
  # Ensure you have Key Vault Secrets User role
  az role assignment list `
    --scope /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.KeyVault/vaults/$kvName `
    --query "[?principalName=='<your-user-email>']" `
    --output table
  ```

3. **Reset VM password** (if secret retrieval works but password is wrong):
  ```powershell
  # This should not be necessary if deployment succeeded
  # Only use as last resort
  az vm user update `
    --resource-group rg-legacyvm-prod `
    --name vm-legacyvm-{random} `
    --username vmadmin `
    --password "NewP@ssw0rd!123"
  ```

### Issue: Bastion Connection Fails

**Symptoms**: Cannot establish Bastion RDP session

**Diagnosis**:
```powershell
# Check Bastion health
az network bastion show `
  --resource-group rg-legacyvm-prod `
  --name bas-legacyvm-{random} `
  --query "{ProvisioningState:provisioningState, DNSName:dnsName}" `
  --output table

# Check VM status
az vm get-instance-view `
  --resource-group rg-legacyvm-prod `
  --name vm-legacyvm-{random} `
  --query "instanceView.statuses[?starts_with(code, 'PowerState/')].displayStatus" `
  --output tsv
```

**Solutions**:
1. **VM is stopped**: Start the VM
  ```powershell
  az vm start --resource-group rg-legacyvm-prod --name vm-legacyvm-{random}
  ```

2. **Bastion NSG rules incorrect**: Verify Bastion subnet NSG
  - Required: Allow inbound 443 from Internet
  - Required: Allow outbound 3389/22 to VirtualNetwork
  - Check: Portal > NSG > nsg-bastion-legacyvm-{random} > Security rules

3. **Browser issues**: Try different browser or incognito mode

### Issue: No Logs in Log Analytics

**Symptoms**: Queries return no results 10+ minutes after deployment

**Diagnosis**:
```kusto
// Check if workspace is receiving any data
Heartbeat
| where TimeGenerated > ago(1h)
| summarize count()

// Check diagnostic settings configuration
AzureDiagnostics
| where TimeGenerated > ago(1h)
| summarize count() by ResourceType
```

**Solutions**:
1. **Wait longer**: Initial log ingestion can take 10-15 minutes
2. **Verify diagnostic settings**:
  ```powershell
  # Check VM diagnostic settings
  az monitor diagnostic-settings list `
    --resource /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.Compute/virtualMachines/vm-legacyvm-{random} `
    --query "value[].{Name:name, LogAnalytics:workspaceId}" `
    --output table
  ```

3. **Manual diagnostic setting creation** (if missing):
  - Portal > VM > Diagnostic settings > Add diagnostic setting
  - Select all log categories and metrics
  - Send to Log Analytics workspace: law-legacyvm-{random}

### Issue: Alerts Not Firing

**Symptoms**: Test conditions met but no alerts visible in Portal

**Diagnosis**:
```powershell
# Check alert rules
az monitor metrics alert list `
  --resource-group rg-legacyvm-prod `
  --query "[].{Name:name, Enabled:enabled, Severity:severity}" `
  --output table

# Check alert condition evaluation
az monitor metrics alert show `
  --resource-group rg-legacyvm-prod `
  --name alert-disk-space-legacyvm-{random} `
  --query "{Enabled:enabled, Condition:criteria, State:properties.state}" `
  --output json
```

**Solutions**:
1. **Wait for evaluation window**: Alerts evaluate every 1-5 minutes
2. **Verify alert is enabled**: Should show `"enabled": true`
3. **Check metric availability**:
  ```powershell
  # List available metrics for VM
  az monitor metrics list-definitions `
    --resource /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.Compute/virtualMachines/vm-legacyvm-{random} `
    --query "[].{Name:name.value, Unit:unit}" `
    --output table
  ```

4. **Review activity log for alert evaluation**:
  - Portal > Monitor > Activity Log
  - Filter: Resource Type = "microsoft.insights/metricalerts"
  - Look for "Evaluate Action" events

## Clean Up Resources

**WARNING**: This will delete ALL resources and data. Ensure you have backups before proceeding.

```powershell
# Delete resource group and all resources
az group delete `
  --name rg-legacyvm-prod `
  --yes `
  --no-wait

# Verify deletion status (takes 5-10 minutes)
az group exists --name rg-legacyvm-prod
# Expected output: false
```

**Cost Estimate**: Keeping resources deployed costs approximately:
- VM (Standard_D2s_v3): ~$70/month
- Storage (1TB file share + disks): ~$50/month
- Bastion: ~$140/month
- Other services (negligible): ~$10/month
- **Total**: ~$270/month in westus3 region

## Next Steps

After successful deployment:

1. **Configure VM**:
  - Install required applications on Windows Server 2016
  - Map file share as network drive: `\\st{random}.file.core.windows.net\fileshare`
  - Configure Windows Firewall rules as needed

2. **Set Up Monitoring**:
  - Configure Log Analytics queries and save as functions
  - Create custom workbooks in Azure Monitor
  - Set up action groups for email/SMS notifications (currently Portal-only)

3. **Implement Backup** (not in scope of this deployment):
  - Azure Backup for VM
  - Azure Files snapshot/backup for file share

4. **Security Hardening** (additional measures):
  - Enable Azure Security Center recommendations
  - Implement Just-In-Time VM access
  - Review and tighten NSG rules based on actual traffic

5. **Operational Procedures**:
  - Document VM maintenance schedules
  - Create runbooks for common tasks
  - Establish change management process

## Support

For issues related to:
- **Bicep**: Review [research.md](./research.md) for module documentation
- **Azure resources**: Check [data-model.md](./data-model.md) for architecture
- **Requirements**: See [spec.md](./spec.md) for detailed specifications
- **Governance**: Review [constitution.md](../../.specify/memory/constitution.md) for principles

For Azure support, visit: https://azure.microsoft.com/support/

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the changes in your project, with a comment of something like Plan created.

5. Checklist (Optional)

Spec Kit uses /speckit.checklist to validate the requirements. The prompt doesn’t require any specific inputs as it analyzes the existing plan for gaps.

Info

To learn more about the checklist step, see the Checklist chapter in the Spec Kit article.

  1. Run the following prompt to generate checklist items for our example:
/speckit.checklist
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the checklist prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like. You can answer in the following format, e.g.: Q1: E, Q2:A, Q3:A
Specify Bootstrap

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Implementation Plan Quality Validation Checklist

**Purpose**: Validate quality, completeness, and clarity of implementation plan and supporting documentation
**Created**: 2026-01-27
**Scope**: Comprehensive review across functional, security, network, and monitoring requirements
**Depth**: Standard (~40 items)
**Feature**: [spec.md](../spec.md) | [plan.md](../plan.md)

---

## Requirement Completeness

Requirements documentation coverage and thoroughness.

- [ ] CHK001 - Are VM compute requirements (CPU, memory, disk) explicitly specified with exact values? [Completeness, Spec Β§FR-001]
- [ ] CHK002 - Are all three network subnet ranges defined with CIDR notation and purpose documented? [Completeness, Spec Β§FR-003]
- [ ] CHK003 - Are storage account requirements specified for both file share quota and performance tier? [Completeness, Spec Β§FR-005]
- [ ] CHK004 - Are availability zone requirements documented with explicit valid range (1-3, never -1)? [Completeness, Spec Β§FR-014]
- [ ] CHK005 - Are diagnostic logging requirements defined for all resource types requiring monitoring? [Completeness, Spec Β§SEC-001]
- [ ] CHK006 - Are all 12 AVM modules documented with exact version numbers and purpose statements? [Completeness, Plan Β§Technical Context]
- [ ] CHK007 - Are deployment sequence phases defined with resource dependencies explicitly stated? [Completeness, Data-Model Β§Deployment Sequence]
- [ ] CHK008 - Are NSG security rules specified for all three subnets with protocol, port, and direction details? [Completeness, Data-Model Β§NSG Configuration]

## Requirement Clarity

Specificity and measurability of requirements to eliminate ambiguity.

- [ ] CHK009 - Is "Standard HDD" quantified with specific Azure SKU names (e.g., StandardSSD_LRS)? [Clarity, Spec Β§FR-001, FR-002]
- [ ] CHK010 - Is "minimal naming" defined with exact pattern format and character count range? [Clarity, Constitution Β§V, Plan Β§Technical Context]
- [ ] CHK011 - Are alert thresholds specified with exact percentage values and evaluation windows? [Clarity, Spec Β§MON-004, Data-Model Β§Alert Configuration]
- [ ] CHK012 - Is "secure remote access" quantified with specific protocol (RDP), port, and authentication method? [Clarity, Spec Β§FR-004]
- [ ] CHK013 - Are "least-privilege NSG rules" defined with concrete allow/deny examples per subnet? [Clarity, Spec Β§SEC-003]
- [ ] CHK014 - Is "deployment within 20 minutes" defined as a measurable success criterion with verification method? [Measurability, Spec Β§SC-001]
- [ ] CHK015 - Is password generation approach explicitly defined with uniqueString() seed sources documented? [Clarity, Research Β§Password Generation]
- [ ] CHK016 - Are resource name length constraints specified with Azure limits (e.g., Storage 24 chars, VM NetBIOS 15 chars)? [Clarity, Spec Β§FR-010, Data-Model Β§Resource Names]

## Requirement Consistency

Alignment and non-contradiction across specification, plan, and supporting documents.

- [ ] CHK017 - Do VM sizing requirements in spec match the data model configuration (Standard_D2s_v3 consistently specified)? [Consistency, Spec Β§FR-001, Data-Model Β§VM Config]
- [ ] CHK018 - Do network CIDR blocks in spec align with data model subnet allocations (10.0.0.0/24 breakdown)? [Consistency, Spec Β§FR-003, Data-Model Β§Network Topology]
- [ ] CHK019 - Do AVM module versions in plan match research document module selections (all 12 modules)? [Consistency, Plan Β§AVM Modules, Research Β§Module Inventory]
- [ ] CHK020 - Do alert requirements in spec match alert configuration in data model (3 critical alerts)? [Consistency, Spec Β§MON-003-005, Data-Model Β§Alerts]
- [ ] CHK021 - Do diagnostic logging requirements align across SEC-001 (spec) and technical context (plan)? [Consistency, Spec Β§SEC-001, Plan Β§Security Baseline]
- [ ] CHK022 - Does naming convention in constitution match implementation in data model? [Consistency, Constitution Β§V, Data-Model Β§Naming Model]
- [ ] CHK023 - Do deployment phases in plan align with dependency graph in data model? [Consistency, Plan Β§Phase 3, Data-Model Β§Deployment Sequence]

## Acceptance Criteria Quality

Measurability and testability of success criteria.

- [ ] CHK024 - Are all 11 success criteria (SC-001 to SC-011) objectively measurable with pass/fail conditions? [Measurability, Spec Β§Success Criteria]
- [ ] CHK025 - Can VM accessibility (SC-004) be verified through documented test procedure in quickstart? [Testability, Spec Β§SC-004, Quickstart Β§4.2]
- [ ] CHK026 - Can NSG rule effectiveness (SC-009) be validated with concrete test scenarios? [Testability, Spec Β§SC-009]
- [ ] CHK027 - Can Log Analytics ingestion (SC-007) be verified within specified 5-minute timeframe? [Measurability, Spec Β§SC-007]
- [ ] CHK028 - Are constitution compliance gates (all 6 principles) verifiable with documented evidence? [Measurability, Plan Β§Constitution Check]

## Scenario Coverage

Completeness of primary, alternate, error, recovery, and non-functional scenarios.

- [ ] CHK029 - Are all five user stories independently testable as documented in spec? [Coverage, Primary Flows, Spec Β§User Scenarios]
- [ ] CHK030 - Are VM computer name length violations (>15 chars) addressed with edge case handling? [Coverage, Edge Case, Spec Β§Edge Cases]
- [ ] CHK031 - Are storage account naming violations (>24 chars, invalid chars) documented as edge cases? [Coverage, Edge Case, Spec Β§Edge Cases]
- [ ] CHK032 - Are private endpoint deployment failures with successful storage account handled? [Coverage, Exception Flow, Spec Β§Edge Cases]
- [ ] CHK033 - Are Key Vault secret access failures monitored with alert configuration? [Coverage, Exception Flow, Spec Β§MON-005]
- [ ] CHK034 - Are availability zone validation requirements (1-3 only, never -1) enforced? [Coverage, Edge Case, Spec Β§FR-014]
- [ ] CHK035 - Are performance requirements addressed for standard HDD selection rationale? [Coverage, Non-Functional, Research Β§Storage Module]

## Edge Case Coverage

Boundary conditions, error states, and exceptional scenarios.

- [ ] CHK036 - Are requirements defined for zero-subnet scenarios or invalid CIDR blocks? [Gap, Edge Case]
- [ ] CHK037 - Are rollback requirements defined if VM deployment succeeds but Key Vault secret creation fails? [Gap, Recovery Flow]
- [ ] CHK038 - Are concurrent deployment conflict scenarios (multiple simultaneous deployments) addressed? [Gap, Edge Case]
- [ ] CHK039 - Are requirements specified for when Log Analytics workspace is unavailable during resource deployment? [Gap, Exception Flow]

## Dependencies & Assumptions

External dependencies, prerequisites, and assumption documentation.

- [ ] CHK040 - Are all Azure resource provider registration requirements documented as prerequisites? [Dependency, Quickstart Β§Prerequisites]
- [ ] CHK041 - Are subscription quota requirements validated as documented assumptions? [Assumption, Spec Β§Assumptions]
- [ ] CHK042 - Are tool version requirements (Bicep 0.33.0+, Azure CLI 2.65.0+) specified with verification commands? [Dependency, Plan Β§Technical Context, Quickstart Β§Prerequisites]
- [ ] CHK043 - Are required Azure permissions documented with specific role names? [Dependency, Quickstart Β§Prerequisites]
- [ ] CHK044 - Is the assumption about Windows Server 2016 image availability in westus3 validated? [Assumption, Spec Β§Assumptions]

## Traceability

Linkage between requirements, specifications, and implementation artifacts.

- [ ] CHK045 - Do all functional requirements (FR-001 to FR-016) have corresponding implementation guidance in plan phases? [Traceability]
- [ ] CHK046 - Do all security requirements (SEC-001 to SEC-008) map to specific AVM module configurations in research? [Traceability]
- [ ] CHK047 - Do all monitoring requirements (MON-001 to MON-005) trace to alert definitions in data model? [Traceability]
- [ ] CHK048 - Do all infrastructure constraints (IC-001 to IC-006) align with constitution principles? [Traceability]

## Ambiguities & Conflicts

Identification of unclear, contradictory, or incomplete requirement areas.

- [ ] CHK049 - Is "HDD-backed file share" disambiguated between Standard_LRS vs other HDD SKUs? [Ambiguity, Spec Β§FR-005]
- [ ] CHK050 - Is "Portal notifications only" for alerts clearly documented as excluding Action Groups? [Clarity, Spec Β§MON-003]
- [ ] CHK051 - Is the deployment timeframe "within 20 minutes" inclusive of validation steps or deployment only? [Ambiguity, Spec Β§SC-001]
- [ ] CHK052 - Are "rich comments" requirements quantified with minimum comment density or coverage percentage? [Ambiguity, Spec Β§FR-015]

---

## Validation Summary

**Total Items**: 52
**Pass Threshold**: β‰₯90% (47+ items checked)
**Review Date**: _________________
**Reviewer**: _________________
**Status**: ☐ PASSED  |  ☐ NEEDS REVISION  |  ☐ BLOCKED

### Issues Identified

_Document any requirement quality issues discovered during validation:_

1.
2.
3.

### Recommendations

_Suggested improvements for requirement clarity, completeness, or testability:_

1.
2.
3.

---

**Next Steps**:
- If PASSED β†’ Proceed to Phase 2 implementation (Bicep template creation)
- If NEEDS REVISION β†’ Address identified issues, re-validate
- If BLOCKED β†’ Escalate missing information to stakeholders

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your new checklist, with a comment of something like Checklist prepared.

6. Tasks

Spec Kit uses /speckit.tasks to generate the tasks.md file. The prompt doesn’t require any specific inputs as it analyzes the existing plan to break it down into actionable tasks. Tasks can be evolved through iterating over the tasks.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.tasks, or leveraging /speckit.analyze to review/validate and challenge the tasks.

Info

To learn more about what the tasks should include, see the Tasks chapter in the Spec Kit article.

  1. Run the following prompt to generate tasks for our example:
/speckit.tasks
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Tasks: Legacy VM Workload Infrastructure

**Feature**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Date**: 2026-01-27
**Branch**: `001-legacy-vm-workload`

**Organization**: Tasks are grouped by user story (US1-US5) to enable independent implementation and testing.

## Format: `[ID] [P?] [Story?] Description`

- **[P]**: Parallelizable (different files, no blocking dependencies)
- **[Story]**: User story label (US1, US2, US3, US4, US5)
- File paths included in descriptions

---

## Phase 1: Setup (Project Initialization)

**Purpose**: Basic project structure and Bicep configuration

- [ ] T001 Create infrastructure directory structure: infra/, infra/docs/
- [ ] T002 Create bicepconfig.json with AVM analyzer rules at infra/bicepconfig.json
- [ ] T003 [P] Create .gitignore file to exclude .bicep build artifacts (*.json from main.bicep compilation)
- [ ] T004 [P] Create project README.md at repository root with quickstart reference
- [ ] T005 Initialize main.bicep with metadata, targetScope='resourceGroup', location parameter

---

## Phase 2: Foundational (Blocking Prerequisites for All User Stories)

**Purpose**: Shared infrastructure that MUST be complete before any user story implementation

**⚠️ CRITICAL**: No user story work can begin until this phase is complete

- [ ] T006 Add parameters to main.bicep: vmSize (default: Standard_D2s_v3), vmAdminUsername (default: vmadmin), vmAdminPasswordSecretName (default: 'vm-admin-password'), availabilityZone (default: 1), fileShareQuotaGiB (default: 1024), logAnalyticsRetentionDays (default: 30)
- [ ] T007 Define variables in main.bicep: suffix = uniqueString(resourceGroup().id), vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, utcNow('u'))}' (NOTE: utcNow() makes deployment non-idempotent - password regenerates on each deploy. Acceptable for initial deployment; consider removing utcNow() for idempotent redeployments)
- [ ] T008 Define resource naming variables: vnetName, vmName, kvName, lawName, stName (storage: no hyphens, max 24 chars)
- [ ] T009 [P] Define tags variable: workload='legacy-vm', environment='production', compliance='legacy-retention', managedBy='bicep-avm'
- [ ] T010 Add AVM module for Log Analytics Workspace (avm/res/operational-insights/workspace:0.15.0) at infra/main.bicep
- [ ] T011 Configure Log Analytics parameters: name, location, retentionInDays, tags
- [ ] T012 Create main.bicepparam file at infra/main.bicepparam with 'using' directive and parameter defaults

**Checkpoint**: Foundation ready - user story phases can now proceed in parallel (if staffed) or sequentially by priority

---

## Phase 3: User Story 1 - Core VM Infrastructure (Priority: P1) 🎯 MVP

**Goal**: Deploy VNet, VM with Windows Server 2016, basic networking - foundational workload infrastructure

**Independent Test**: Deploy to test resource group, verify VM created with correct specs (Standard_D2s_v3, Windows Server 2016), VM communicates within VNet

### Validation for User Story 1 (MANDATORY - Constitution Principle III) ⚠️

> **NOTE: These validation tasks must be executed BEFORE deployment**

- [ ] T013 [US1] Run `bicep build infra/main.bicep` to compile and check syntax errors
- [ ] T014 [US1] Run `az deployment group validate --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T015 [US1] Run `az deployment group what-if --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T016 [US1] Review what-if output: verify VNet, VM, NIC will be created with no unexpected changes

### Implementation for User Story 1

- [ ] T017 [P] [US1] Add AVM module for Virtual Network (avm/res/network/virtual-network:0.7.2) in main.bicep
- [ ] T018 [US1] Configure VNet parameters: name, location, addressPrefixes=['10.0.0.0/24'], subnets array with 3 subnets (VM: 10.0.0.0/27, Bastion: 10.0.0.64/26, PE: 10.0.0.128/27)
- [ ] T019 [US1] Add diagnostic settings to VNet module: send to Log Analytics workspace ID reference
- [ ] T020 [P] [US1] Add AVM module for Virtual Machine (avm/res/compute/virtual-machine:0.21.0) in main.bicep
- [ ] T021 [US1] Configure VM parameters: name='vm-legacyvm-${suffix}', computerName='vm-${substring(suffix,0,10)}' (≀15 chars), size=vmSize parameter, adminUsername=vmAdminUsername, adminPassword=kvSecretReference, zone=availabilityZone
- [ ] T022 [US1] Configure VM OS: imageReference for Windows Server 2016, osDisk with Standard_LRS SKU (HDD performance tier)
- [ ] T023 [US1] Configure VM managed identity: type='SystemAssigned'
- [ ] T024 [US1] Configure VM NIC: attach to VM subnet, no public IP, dynamic private IP
- [ ] T025 [US1] Add VM diagnostic settings to Log Analytics workspace
- [ ] T026 [US1] Add VM outputs: vmName, vmResourceId, vmPrivateIP

### Deployment for User Story 1

- [ ] T027 [US1] Create Azure resource group: `az group create --name rg-legacyvm-test --location westus3`
- [ ] T028 [US1] Deploy to test resource group: `az deployment group create --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T029 [US1] Verify VNet created with 3 subnets in Azure Portal (10.0.0.0/24 address space)
- [ ] T030 [US1] Verify VM created with Windows Server 2016, Standard_D2s_v3, correct zone
- [ ] T031 [US1] Verify VM computer name is ≀15 characters (NetBIOS limit)
- [ ] T032 [US1] Verify diagnostic logs flowing to Log Analytics workspace within 5 minutes

**Checkpoint**: User Story 1 complete - VM infrastructure deployed and validated. Ready to proceed with US2 and US3 in parallel.

---

## Phase 4: User Story 2 - Secure Storage and Data Disk (Priority: P2)

**Goal**: Attach 500GB data disk to VM, deploy storage account with 1TB file share via private endpoint

**Independent Test**: Verify 500GB HDD data disk attached to VM, file share accessible from VM through private endpoint (no internet traversal)

### Implementation for User Story 2

- [ ] T033 [P] [US2] Add data disk to VM module configuration in main.bicep: dataDisks array with disk size=500, sku=Standard_LRS, lun=0, name='datadisk-01'
- [ ] T034 [P] [US2] Add AVM module for Storage Account (avm/res/storage/storage-account:0.31.0) in main.bicep
- [ ] T035 [US2] Configure storage parameters: name='st${replace(suffix, '-', '')}' (max 24 chars, no hyphens), kind='StorageV2', sku='Standard_LRS', accessTier='Hot', publicNetworkAccess='Disabled'
- [ ] T036 [US2] Configure file share in storage module: fileServices with share name='fileshare', quota=fileShareQuotaGiB (1024 GiB)
- [ ] T037 [US2] Add diagnostic settings to storage module: send to Log Analytics workspace
- [ ] T038 [P] [US2] Add AVM module for Private DNS Zone (avm/res/network/private-dns-zone:0.8.0) in main.bicep
- [ ] T039 [US2] Configure DNS zone name: 'privatelink.file.core.windows.net', VNet link to main VNet
- [ ] T040 [P] [US2] Add AVM module for Private Endpoint (avm/res/network/private-endpoint:0.11.1) in main.bicep
- [ ] T041 [US2] Configure private endpoint: subnet=PE subnet, groupIds=['file'], privateDnsZoneResourceIds=[DNS zone ID], link to storage account resource
- [ ] T042 [US2] Add storage outputs: storageAccountName, fileShareName, privateEndpointIP

### Deployment for User Story 2

- [ ] T043 [US2] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T044 [US2] Deploy updated template to test resource group
- [ ] T045 [US2] Verify 500GB data disk attached to VM in Azure Portal (LUN 0, Standard_LRS)
- [ ] T046 [US2] Verify storage account created with public access disabled
- [ ] T047 [US2] Verify file share created with 1024 GiB quota
- [ ] T048 [US2] Verify private endpoint resolves to internal IP (10.0.0.128/27 range): `nslookup st{random}.file.core.windows.net` from VM
- [ ] T049 [US2] Test file share access from VM via Bastion: `Test-NetConnection -ComputerName st{random}.file.core.windows.net -Port 445`

**Checkpoint**: User Stories 1 AND 2 complete - VM with data disk and storage file share via private endpoint validated.

---

## Phase 5: User Story 3 - Secure Access and Secrets Management (Priority: P2)

**Goal**: Deploy Azure Bastion for secure RDP access, Key Vault for storing VM password

**Independent Test**: Connect to VM through Bastion host using password retrieved from Key Vault (no public IP on VM)

### Implementation for User Story 3

- [ ] T050 [P] [US3] Add AVM module for Key Vault (avm/res/key-vault/vault:0.13.3) in main.bicep
- [ ] T051 [US3] Configure Key Vault parameters: name='kv-legacyvm-${suffix}', sku='standard', enableRbacAuthorization=true, softDeleteRetentionInDays=90
- [ ] T052 [US3] Add Key Vault secret via module's secrets parameter: name=vmAdminPasswordSecretName parameter, value=vmPassword variable, contentType='text/plain'
- [ ] T053 [US3] Add RBAC role assignment in Key Vault module: principalId=VM managed identity, roleDefinitionIdOrName='Key Vault Secrets User'
- [ ] T054 [US3] Add Key Vault diagnostic settings to Log Analytics workspace
- [ ] T055 [P] [US3] Update VM module configuration: change adminPassword to reference Key Vault secret (use getSecret() or secretReference)
- [ ] T056 [P] [US3] Add AVM module for Bastion Host (avm/res/network/bastion-host:0.8.2) in main.bicep
- [ ] T057 [US3] Configure Bastion parameters: name='bas-legacyvm-${suffix}', sku='Basic', vnetId=VNet resource ID, subnetName='AzureBastionSubnet'
- [ ] T058 [US3] Add Bastion diagnostic settings to Log Analytics workspace
- [ ] T059 [US3] Add Key Vault and Bastion outputs: kvName, kvResourceId, bastionName, bastionResourceId

### Deployment for User Story 3

- [ ] T060 [US3] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T061 [US3] Deploy updated template to test resource group (expected duration: 15-20 minutes for Bastion)
- [ ] T062 [US3] Verify Key Vault created with RBAC enabled (not access policies)
- [ ] T063 [US3] Verify VM password stored as Key Vault secret: `az keyvault secret show --name vm-admin-password --vault-name kv-legacyvm-{suffix}`
- [ ] T064 [US3] Verify VM managed identity has 'Key Vault Secrets User' role on Key Vault
- [ ] T065 [US3] Verify Azure Bastion deployed successfully in Bastion subnet
- [ ] T066 [US3] Test Bastion connectivity: Connect to VM via Azure Portal β†’ VM β†’ Connect β†’ Bastion, use username='vmadmin' and password from Key Vault
- [ ] T067 [US3] Verify VM has no public IP address assigned (confirm access only through Bastion)

**Checkpoint**: User Stories 1, 2, AND 3 complete - Full VM infrastructure with secure access and storage operational.

---

## Phase 6: User Story 4 - Internet Connectivity and Network Security (Priority: P3)

**Goal**: Configure NAT Gateway for outbound internet, implement NSGs for all subnets with least-privilege rules

**Independent Test**: Verify VM reaches internet through NAT Gateway, NSG rules block unauthorized traffic

### Implementation for User Story 4

- [ ] T068 [P] [US4] Add AVM module for NAT Gateway (avm/res/network/nat-gateway:2.0.1) in main.bicep
- [ ] T069 [US4] Configure NAT Gateway parameters: name='nat-legacyvm-${suffix}', zone=availabilityZone, publicIpAddressObjects=[{name: 'pip-nat'}]
- [ ] T070 [US4] Update VNet module configuration: associate NAT Gateway with VM subnet (natGatewayId in subnet definition)
- [ ] T071 [US4] Add NAT Gateway diagnostic settings to Log Analytics workspace
- [ ] T072 [P] [US4] Add AVM module for VM Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T073 [US4] Configure VM NSG security rules: **CRITICAL - inbound allow TCP 3389 from Bastion subnet (10.0.0.64/26) priority 100**, inbound deny all (priority 4096), outbound allow Internet (priority 100), outbound allow VNet (priority 200), outbound deny all (priority 4096)
- [ ] T074 [US4] Update VNet module: associate VM NSG with VM subnet (networkSecurityGroupId in subnet definition)
- [ ] T075 [US4] Add VM NSG diagnostic settings to Log Analytics workspace
- [ ] T076 [P] [US4] Add AVM module for Bastion Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T077 [US4] Configure Bastion NSG security rules: inbound allow 443 from Internet, allow GatewayManager 443, allow AzureLoadBalancer 443, allow Bastion communication 8080/5701; outbound allow SSH/RDP to VNet, allow Azure Cloud 443, allow Bastion communication, allow HTTP 80
- [ ] T078 [US4] Update VNet module: associate Bastion NSG with Bastion subnet
- [ ] T079 [US4] Add Bastion NSG diagnostic settings to Log Analytics workspace
- [ ] T080 [P] [US4] Add AVM module for PE Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T081 [US4] Configure PE NSG security rules: inbound allow TCP 445 from VM subnet (10.0.0.0/27), inbound deny all; outbound allow all
- [ ] T082 [US4] Update VNet module: associate PE NSG with PE subnet
- [ ] T083 [US4] Add PE NSG diagnostic settings to Log Analytics workspace
- [ ] T084 [US4] Add NSG and NAT Gateway outputs: nsgVmName, nsgBastionName, nsgPeName, natGatewayName

### Deployment for User Story 4

- [ ] T085 [US4] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T086 [US4] Deploy updated template to test resource group
- [ ] T087 [US4] Verify NAT Gateway created with public IP and associated with VM subnet
- [ ] T088 [US4] Verify 3 NSGs created and associated with correct subnets
- [ ] T089 [US4] Test outbound internet from VM via Bastion RDP session: `Test-NetConnection -ComputerName google.com -Port 443` (should succeed through NAT Gateway)
- [ ] T090 [US4] Test NSG deny rules: attempt unauthorized inbound connection to VM (should be blocked)
- [ ] T091 [US4] Verify all NSG diagnostic logs flowing to Log Analytics workspace

**Checkpoint**: User Stories 1-4 complete - Full network security and internet connectivity operational.

---

## Phase 7: User Story 5 - Monitoring and Alerting (Priority: P3)

**Goal**: Configure diagnostic settings for all resources, deploy 3 critical alerts (VM stopped, disk >85%, Key Vault access failures)

**Independent Test**: Verify diagnostic logs flowing to Log Analytics, trigger test alert scenarios and confirm alerts fire

### Implementation for User Story 5

- [ ] T092 [P] [US5] Add AVM module for VM Stopped Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T093 [US5] Configure VM stopped alert: name='alert-vm-stopped-legacyvm-${suffix}', targetResourceId=VM resource ID, metricName='Percentage CPU', operator='LessThan', threshold=1, aggregation='Average', windowSize='PT15M', severity=0 (Critical), enabled=true, autoMitigate=false
- [ ] T094 [US5] Ensure no action groups configured (Portal-only notifications per MON-003)
- [ ] T095 [P] [US5] Add AVM module for Disk Space Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T096 [US5] Configure disk space alert: name='alert-disk-space-legacyvm-${suffix}', targetResourceId=VM resource ID, metricName='OS Disk Used Percentage', operator='GreaterThan', threshold=85, aggregation='Average', windowSize='PT5M', severity=0, enabled=true, autoMitigate=false
- [ ] T097 [P] [US5] Add AVM module for Key Vault Access Failure Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T098 [US5] Configure KV alert: name='alert-kv-access-fail-legacyvm-${suffix}', targetResourceId=Key Vault resource ID, metricName='ServiceApiHit', dimensions=[{name: 'ActivityName', operator: 'Include', values: ['SecretGet']}, {name: 'StatusCode', operator: 'Include', values: ['Unauthorized']}], operator='GreaterThan', threshold=0, aggregation='Count', windowSize='PT5M', severity=0
- [ ] T099 [US5] Review all existing resource modules: verify diagnostic settings already configured for VNet, VM, Storage, Key Vault, NSGs, NAT Gateway, Bastion (completed in previous phases)
- [ ] T100 [US5] Add alert outputs: alertVmStoppedName, alertDiskSpaceName, alertKvFailureName

### Deployment for User Story 5

- [ ] T101 [US5] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T102 [US5] Deploy updated template to test resource group
- [ ] T103 [US5] Verify Log Analytics workspace contains logs from all resources: run query in Azure Portal β†’ Log Analytics β†’ Logs β†’ `AzureDiagnostics | where ResourceGroup == 'rg-legacyvm-test' | summarize count() by ResourceType`
- [ ] T104 [US5] Verify 3 metric alerts created and enabled in Azure Portal β†’ Monitor β†’ Alerts
- [ ] T105 [US5] Test VM stopped alert: Stop VM, wait 15 minutes, verify alert fires and visible in Portal
- [ ] T106 [US5] Test Key Vault access failure alert: Attempt to access non-existent secret `az keyvault secret show --name fake-secret --vault-name kv-legacyvm-{suffix}`, wait 5 minutes, verify alert fires
- [ ] T107 [US5] Verify alert notifications visible in Azure Portal β†’ Monitor β†’ Alerts (no external action groups configured)
- [ ] T108 [US5] Document alert testing procedures in infra/docs/deployment.md

**Checkpoint**: All 5 user stories complete - Full monitoring and alerting operational. MVP infrastructure complete!

---

## Phase 8: Polish & Cross-Cutting Concerns

**Purpose**: Final improvements and documentation that span multiple user stories

- [ ] T109 [P] Review main.bicep for code quality: verify all resources have comments explaining purpose
- [ ] T110 [P] Review main.bicepparam for documentation: verify parameter descriptions and defaults documented
- [ ] T111 [P] Validate constitution compliance: check all 6 principles satisfied (IC-001 to IC-006, SEC-001 to SEC-008)
- [ ] T112 [P] Run final validation suite: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T113 Update infra/docs/deployment.md with full deployment outcomes and lessons learned
- [ ] T114 [P] Create architecture diagram in infra/docs/architecture.md showing all resources and dependencies
- [ ] T115 Verify all success criteria met (SC-001 to SC-011): deployment time <20 minutes, all resources operational, logs flowing, alerts working
- [ ] T116 Execute quickstart.md validation end-to-end: follow deployment guide steps, verify successful deployment
- [ ] T117 [P] Review Azure Security Center recommendations: address any high-severity compliance issues
- [ ] T118 Document estimated monthly costs in README.md: VM ~$70, Storage ~$50, Bastion ~$140, Other ~$10 = Total ~$270/month
- [ ] T119 Create CHANGELOG.md entry: document initial deployment date, version 1.0.0, all resources deployed
- [ ] T120 Final code review: verify no hardcoded values, all parameters in main.bicepparam, rich comments present

**Checkpoint**: Production-ready infrastructure complete. Ready for operational handoff.

---

## Dependencies & Execution Order

### Phase Dependencies

1. **Setup (Phase 1)**: No dependencies - start immediately
2. **Foundational (Phase 2)**: Depends on Setup - BLOCKS all user stories
3. **User Story 1 (Phase 3)**: Depends on Foundational - MVP foundation, MUST complete first
4. **User Story 2 (Phase 4)**: Depends on Foundational - Can proceed after US1 or in parallel
5. **User Story 3 (Phase 5)**: Depends on Foundational and US1 (VM must exist for Key Vault password reference) - Bastion depends on VNet from US1
6. **User Story 4 (Phase 6)**: Depends on Foundational and US1 (NAT Gateway and NSGs associate with VNet subnets from US1)
7. **User Story 5 (Phase 7)**: Depends on all previous user stories (alerts target VM, Key Vault; diagnostic settings reference Log Analytics from Foundational)
8. **Polish (Phase 8)**: Depends on all user stories complete

### User Story Interdependencies

- **US1 (Core VM)**: Independent after Foundational - can start first
- **US2 (Storage)**: Depends on US1 (data disk attaches to VM, private endpoint needs VNet)
- **US3 (Secure Access)**: Depends on US1 (Key Vault stores password for VM, Bastion accesses VM, both need VNet)
- **US4 (Network Security)**: Depends on US1 (NAT Gateway and NSGs associate with VNet subnets)
- **US5 (Monitoring)**: Depends on US1, US3 (alerts target VM and Key Vault resources)

### Recommended Execution Sequence

**Option 1 - Sequential by Priority** (single developer):
1. Setup β†’ Foundational β†’ US1 β†’ US2 β†’ US3 β†’ US4 β†’ US5 β†’ Polish

**Option 2 - Parallel with Blocking** (team of 3):
1. Setup β†’ Foundational
2. US1 (Developer 1)
3. After US1: US2 + US3 + US4 in parallel (Developers 1, 2, 3)
4. After US2/US3/US4: US5 (any developer)
5. Polish

### Validation Cadence

- Run `bicep build` after every file modification
- Run `az deployment validate` before every deployment
- Run `what-if` analysis before every deployment to test/production
- Test each user story independently after its deployment phase

### MVP Definition

**Minimum Viable Product = User Story 1 Complete**

At T032 completion, you have:
- Windows Server 2016 VM operational
- VNet with 3 subnets configured
- Basic infrastructure validated

This is sufficient to demonstrate foundational workload capabilities. Subsequent user stories add storage, secure access, network security, and monitoring incrementally.

---

## Parallel Execution Opportunities

### Tasks That Can Run in Parallel

**Phase 1 (Setup)**: T003, T004 can run in parallel with T002

**Phase 2 (Foundational)**: T009, T012 can run in parallel after T008

**Phase 3 (US1)**: T017, T020 can run in parallel (research different modules)

**Phase 4 (US2)**: T033, T034, T038, T040 can run in parallel (different module additions)

**Phase 5 (US3)**: T050, T055, T056 can run in parallel (Key Vault, Bastion, VM update)

**Phase 6 (US4)**: T068, T072, T076, T080 can run in parallel (NAT Gateway + 3 NSGs)

**Phase 7 (US5)**: T092, T095, T097 can run in parallel (3 independent alert modules)

**Phase 8 (Polish)**: T109, T110, T111, T114, T117, T118 can run in parallel (different file updates)

### Example Parallel Workflow (3 developers)

**Sprint 1 (Week 1)**:
- Dev 1: T001-T012 (Setup + Foundational)
- Dev 2: T003-T004 in parallel with Dev 1
- Dev 3: Start planning US2 tasks

**Sprint 2 (Week 2)**:
- Dev 1: T013-T032 (US1 - Core VM)
- Dev 2 + Dev 3: Prepare for US2/US3 parallel work

**Sprint 3 (Week 3)**:
- Dev 1: T033-T049 (US2 - Storage)
- Dev 2: T050-T067 (US3 - Secure Access)
- Dev 3: T068-T091 (US4 - Network Security)

**Sprint 4 (Week 4)**:
- Dev 1: T092-T108 (US5 - Monitoring)
- Dev 2 + Dev 3: T109-T120 (Polish) in parallel

**Total Duration**: ~4 weeks with 3 developers, or ~6 weeks sequential

---

## Implementation Strategy

### MVP-First Approach

1. **Deliver US1 first** (Phase 3: Core VM Infrastructure)
  - Provides foundational value: operational VM with networking
  - Independent testable increment
  - Validates Bicep template structure and AVM module usage

2. **Add US2 + US3** (Phase 4-5: Storage + Secure Access)
  - Provides secure operations capabilities
  - Data storage layer complete
  - Bastion access operational

3. **Add US4 + US5** (Phase 6-7: Network Security + Monitoring)
  - Production-grade security and observability
  - Complete infrastructure compliance

### Incremental Delivery Value

| Completion Point | Value Delivered | Can Deploy to Production? |
|------------------|-----------------|---------------------------|
| After US1 | Basic VM workload operational | No - missing security controls |
| After US1 + US3 | VM with secure access | No - missing storage and monitoring |
| After US1 + US2 + US3 | VM with storage and secure access | Maybe - basic functionality complete, but no monitoring |
| After US1-US4 | Full network security in place | Maybe - functionally complete, limited observability |
| After US1-US5 | Full monitoring and alerting | **YES** - production-ready |

### Rollback Strategy

- Each user story deployment is incremental - previous state preserved
- ARM deployment mode: Incremental (default) - only adds/updates resources
- Rollback: Redeploy previous version of main.bicep (if maintained in git)
- Nuclear option: Delete resource group and redeploy from scratch (acceptable for this single-RG workload)

---

## Task Validation Checklist

Before marking tasks.md as complete, verify:

- [ ] All 5 user stories from spec.md mapped to task phases
- [ ] Each user story has validation tasks (T013-T016 pattern for Bicep/ARM validation)
- [ ] Each user story has implementation tasks with AVM module references
- [ ] Each user story has deployment tasks with verification steps
- [ ] Each user story is independently testable (checkpoint verifications defined)
- [ ] All 16 functional requirements (FR-001 to FR-016) covered in tasks
- [ ] All 8 security requirements (SEC-001 to SEC-008) covered in tasks
- [ ] All 6 infrastructure constraints (IC-001 to IC-006) covered in tasks
- [ ] All 5 monitoring requirements (MON-001 to MON-005) covered in tasks
- [ ] Task IDs sequential (T001 to T120)
- [ ] Parallel tasks marked with [P]
- [ ] User story tasks marked with [US1] to [US5]
- [ ] File paths included in task descriptions
- [ ] Constitution validation tasks included (T111)
- [ ] Quickstart validation task included (T116)

**Total Tasks**: 120
**Estimated Duration**: 4-6 weeks (depending on team size and parallel execution)

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your task list, with a comment of something like Tasks generated.

7. Analyze (Optional)

Spec Kit uses /speckit.analyze to generate an analysis report. The prompt doesn’t require any specific inputs as it analyzes the existing spec, plan and tasks to produce the report.

Info

To learn more about the analyze step, see the Analyze chapter in the Spec Kit article.

  1. Run the following prompt to generate an analysis report for our example:
/speckit.analyze
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Specification Analysis Report

**Feature**: 001-legacy-vm-workload
**Analyzed Files**: spec.md, plan.md, tasks.md, constitution.md
**Date**: 2026-01-26
**Analysis Mode**: READ-ONLY (no modifications applied)

---

## Executive Summary

Analyzed **24 functional requirements**, **4 user stories**, and **61 implementation tasks** across the specification artifacts. Identified **16 findings** (0 CRITICAL, 5 HIGH, 8 MEDIUM, 3 LOW). Constitution compliance: **100% aligned** with all 5 principles satisfied. Primary concerns: **ambiguous terminology** (Standard HDD), **missing technical specifications** (AVM module versions, NSG rules, encryption details), and **underspecified monitoring thresholds**.

**Overall Assessment**: βœ… **SAFE TO PROCEED** with implementation after resolving 5 HIGH severity issues. No CRITICAL or constitution-blocking issues detected.

---

## Findings

| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Ambiguity | HIGH | spec.md:FR-003, FR-008, tasks.md:T025, T030, T033 | "Standard HDD" not mapped to explicit Azure SKU (Standard_LRS vs StandardSSD_LRS) | Specify `Standard_LRS` explicitly in spec FR-003, FR-008; update tasks T025, T030, T033 with SKU name |
| A2 | Underspecification | HIGH | spec.md:FR-022, tasks.md:T040 | "Disk utilization >90%" threshold ambiguous - percentage of what? (capacity, IOPS, throughput) | Clarify as "OS disk used capacity >90%" or "Data disk used capacity >90%"; specify which disk(s) |
| A3 | Underspecification | HIGH | spec.md:FR-020, plan.md, tasks.md:T012 | NSG rules incomplete - missing explicit allow rules for Key Vault (443), storage (445), Log Analytics (443) | Document required outbound NSG rules: Azure services (AzureCloud service tag) port 443 for KV/Storage/LA |
| A4 | Coverage Gap | HIGH | spec.md:FR-011, tasks.md:T013-T015 | Password complexity requirements not validated against Azure VM password policy (12-123 chars, 3 of 4 types) | Add validation task or clarify password generation formula meets Azure requirements |
| A5 | Underspecification | HIGH | All documents | AVM module version constraints missing - no "latest stable" definition or version pinning strategy | Add research task to query MCR for latest versions; specify version pinning strategy (exact vs ^0.x) |
| U1 | Underspecification | MEDIUM | spec.md:FR-019, tasks.md:T016, T020, T027, T036 | Diagnostic log categories not specified - which logs to collect per resource type? | Define log categories: VM (Performance, Security), KV (AuditEvent), Storage (Transaction, StorageRead) |
| U2 | Inconsistency | MEDIUM | spec.md:US4 acceptance scenario, plan.md | Key Vault access model conflict - spec mentions "access policies", plan says "RBAC". Which to use? | Resolve: Use RBAC per plan (T015 specifies enableRbacAuthorization: true); update spec to remove access policies reference |
| U3 | Coverage Gap | MEDIUM | spec.md, tasks.md | Encryption-at-rest requirements implicit but not explicit for VM disks, storage account, Key Vault | Add requirement: "All storage must use Azure-managed encryption at rest" or assume default encryption |
| U4 | Underspecification | MEDIUM | spec.md:FR-022, tasks.md:T039-T041 | Alert notification destinations undefined - who receives alerts? Email? Action group? | Document assumption: Alert rules created without notification actions (configured post-deployment) |
| U5 | Inconsistency | MEDIUM | spec.md:FR-016, plan.md, tasks.md:T007 | Availability zone terminology: spec says "selection", plan/tasks say "parameter" - is it user choice or fixed? | Clarify: availabilityZone is a parameter with default 1; user can select 1, 2, or 3 (not random assignment) |
| U6 | Underspecification | MEDIUM | spec.md:FR-024, plan.md | File share growth monitoring "documented procedures" - what procedures? Manual check? Alert? | Specify: Document manual procedure to query file share usage via Azure CLI/Portal; no automated monitoring |
| U7 | Coverage Gap | MEDIUM | spec.md, tasks.md:T028 | VM boot diagnostics storage account not specified - uses managed storage or custom? | Add clarification: Use managed boot diagnostics (no separate storage account); T028 should specify this |
| U8 | Underspecification | MEDIUM | spec.md:FR-014, plan.md, tasks.md:T009 | Random suffix generation uses uniqueString(resourceGroup().id) - what's the character length? | Specify: 6-character suffix using substring(uniqueString(resourceGroup().id), 0, 6) |
| T1 | Terminology Drift | LOW | spec.md uses "Azure Storage Account", tasks.md uses "Storage Account" | Minor inconsistency in terminology - not functionally impactful | Standardize on "Storage Account" throughout |
| T2 | Duplication | LOW | spec.md:FR-005, FR-017 + plan.md Technical Context | Resource group designation repeated - spec says "production", plan implies via parameter 'environment' | Consolidate: Resource group is deployment target (not created by template); environment tag controlled by parameter |
| T3 | Coverage Gap | LOW | tasks.md:T002 | No task detail for creating bicepconfig.json analyzer rules despite constitution principle IV requiring it | Add detail to T002: Include analyzer rule `use-recent-module-versions: warning` in bicepconfig.json |

---

## Coverage Summary

### Requirements-to-Tasks Mapping

| Requirement Key | Has Task? | Task IDs | Coverage Status |
|-----------------|-----------|----------|-----------------|
| FR-001 (WinServer2016 US West 3) | βœ… | T006, T023 | βœ… Covered by location param + imageReference |
| FR-002 (2 cores, 8GB RAM) | βœ… | T023 | βœ… vmSize parameter (Standard_D2s_v3) |
| FR-003 (Standard HDD OS) | βœ… | T025 | ⚠️ Ambiguous - need SKU clarification (A1) |
| FR-004 (500GB data disk) | βœ… | T030 | ⚠️ Ambiguous - need SKU clarification (A1) |
| FR-005 (Single RG) | βœ… | Implicit | βœ… Deployment target (no task needed) |
| FR-006 (VNet) | βœ… | T011 | βœ… 3 subnets defined |
| FR-007 (Bastion) | βœ… | T018-T021 | βœ… Fully covered |
| FR-008 (Storage + file share) | βœ… | T032-T033 | ⚠️ Ambiguous SKU (A1) |
| FR-009 (Private endpoint) | βœ… | T035 | βœ… Covered |
| FR-010 (Key Vault) | βœ… | T014 | βœ… Covered |
| FR-011 (Password gen + store) | βœ… | T013, T015 | ⚠️ Complexity validation gap (A4) |
| FR-012 (Username = administrator) | βœ… | T024, T053 | βœ… Covered |
| FR-013 (Secret name param) | βœ… | T015, T053 | βœ… Covered |
| FR-014 (Naming convention) | βœ… | T009 + all resources | ⚠️ Length not specified (U8) |
| FR-015 (AVM modules only) | βœ… | All module tasks | ⚠️ No version constraints (A5) |
| FR-016 (Zones 1-3) | βœ… | T007 | ⚠️ Terminology inconsistency (U5) |
| FR-017 (Parameter-driven) | βœ… | T005-T008, T053 | βœ… Covered |
| FR-018 (CanNotDelete locks) | βœ… | T017, T021, T029, T031, T037, T038 | βœ… Covered |
| FR-019 (Diagnostic logging) | βœ… | T016, T020, T027, T036 | ⚠️ Log categories unspecified (U1) |
| FR-020 (NSG rules) | βœ… | T012 | ⚠️ Incomplete rules (A3) |
| FR-021 (No public access) | βœ… | T034 | βœ… Covered |
| FR-022 (Critical alerts) | βœ… | T039-T041 | ⚠️ Threshold + notification gaps (A2, U4) |
| FR-023 (Log Analytics) | βœ… | T010 | βœ… Covered |
| FR-024 (File share growth) | βœ… | Documented in spec | ⚠️ Procedure undefined (U6) |

**Coverage %**: **100%** (24/24 requirements have associated tasks)

### User Story Coverage

| User Story | Priority | Tasks | Coverage Status |
|------------|----------|-------|-----------------|
| US1 - Core VM Infrastructure | P1 (MVP) | T018-T021, T022-T029 | βœ… Fully covered (9 tasks) |
| US2 - Attach Additional Storage | P2 | T030-T031 | βœ… Fully covered (2 tasks) |
| US3 - Connect to Secure File Share | P3 | T032-T038 | βœ… Fully covered (7 tasks) |
| US4 - Secure Secret Management | P1 (MVP) | T013-T017 | βœ… Fully covered (5 tasks) |

**User Story Coverage**: 100% (all 4 stories have complete task coverage)

---

## Constitution Alignment Issues

**Status**: βœ… **ZERO VIOLATIONS** - All 5 constitution principles are fully satisfied

| Principle | Status | Evidence | Risk Level |
|-----------|--------|----------|------------|
| I. AVM-Only Modules | βœ… PASS | Tasks T010-T041 reference only AVM modules from br/public:avm/... registry; zero direct resource declarations planned | None |
| II. IaC-First Approach | βœ… PASS | Single main.bicep (T004), no custom scripts/CSE, ARM incremental mode, manual post-deployment file share mounting (T058) | None |
| III. Security & Reliability | βœ… PASS | Private endpoints (T035), Key Vault (T014-T015), CanNotDelete locks (T017, T021, T029, T031, T037, T038), diagnostic logs (T016, T020, T027, T036), NSG (T012) | None |
| IV. Pre-Deployment Validation | βœ… PASS | Tasks T054-T055 explicitly mandate `az deployment group validate` + ARM What-If review before deployment (T056) | None |
| V. Naming Convention & Regional Standards | βœ… PASS | Naming via T009 (uniqueString suffix), location=westus3 (T006), zones 1-3 (T007), CAF abbreviations in all resource names | None |

**Constitution Compliance Score**: 100% (5/5 principles satisfied)

---

## Unmapped Tasks

**Status**: βœ… **ZERO UNMAPPED TASKS**

All 61 tasks map to at least one requirement, user story, or infrastructure prerequisite:
- **Setup tasks (T001-T003)**: Infrastructure initialization (required for all user stories)
- **Foundational tasks (T004-T012)**: Shared resources (Log Analytics, VNet, NSG) - prerequisite for all user stories
- **User story tasks (T013-T038)**: Direct mapping to US1, US2, US3, US4
- **Polish tasks (T039-T053)**: Cross-cutting concerns (alerts, outputs, parameters)
- **Validation tasks (T054-T061)**: Post-implementation verification

---

## Metrics

### Quantitative Analysis

- **Total Functional Requirements**: 24 (FR-001 through FR-024)
- **Total User Stories**: 4 (US1-P1, US2-P2, US3-P3, US4-P1)
- **Total Implementation Tasks**: 61 (T001 through T061)
- **Total Entities (Data Model)**: 12 Azure resources
- **Total Input Parameters**: 11 (3 required, 8 optional with defaults)
- **Total Output Values**: 10

### Coverage Metrics

- **Requirements Coverage**: 100% (24/24 requirements have β‰₯1 task)
- **User Story Coverage**: 100% (4/4 stories have complete task sets)
- **Constitution Compliance**: 100% (5/5 principles satisfied)
- **Task-to-Requirement Traceability**: 100% (all 61 tasks map to requirements or prerequisites)

### Quality Metrics

- **Ambiguity Count**: 5 findings (A1, A2, U1, U2, U8)
- **Duplication Count**: 1 finding (T2)
- **Underspecification Count**: 9 findings (A2, A3, A4, A5, U1, U4, U6, U7, U8)
- **Coverage Gaps**: 4 findings (A4, U3, U7, T3)
- **Inconsistencies**: 2 findings (U2, U5)
- **Terminology Drift**: 1 finding (T1)

### Severity Distribution

- **CRITICAL Issues**: 0 (deployment blockers)
- **HIGH Issues**: 5 (implementation ambiguity, missing technical specs)
- **MEDIUM Issues**: 8 (specification gaps, inconsistencies)
- **LOW Issues**: 3 (documentation polish, minor gaps)
- **Total Findings**: 16

---

## Dependency Analysis

### Critical Path Dependencies

```
Log Analytics (T010)
    ↓
VNet + NSG (T011-T012) ────┐
    ↓                       β”‚
Key Vault + Password ────────
(T013-T017)                 β”‚
    ↓                       β”‚
VM + Bastion β†β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(T018-T029)
    ↓
Data Disk (T030-T031)
    ↓
Storage + Private Endpoint (T032-T038)
    ↓
Alerts + Outputs (T039-T051)
    ↓
Validation (T054-T061)
```

### Parallelization Opportunities

**35 tasks marked [P]** can execute in parallel:

1. **Phase 2 - Foundational** (after Log Analytics + VNet):
  - T005-T008 (Parameters) - 4 tasks in parallel
  - T012 (NSG) can overlap with T010-T011

2. **Phase 3 - MVP** (after Key Vault complete):
  - T018-T021 (Bastion) parallel with T022-T029 (VM) - both depend on VNet only

3. **Phase 4-5** (after Foundation):
  - T030-T031 (Data Disk) parallel with T032-T038 (Storage + PE) - no mutual dependency

4. **Phase 6 - Polish**:
  - T039-T041 (Alert Rules) - 3 tasks in parallel
  - T042-T051 (Outputs) - 10 tasks in parallel
  - T052-T053 (Parameters file) parallel with T039-T051

**Estimated Parallelization Benefit**: ~30% time reduction (from 8.5 hours to ~6 hours with optimal parallelization)

---

## Next Actions

### Before `/speckit.implement` (MANDATORY)

**1. RESOLVE HIGH SEVERITY ISSUES (A1-A5)**:

- **A1 - Standard HDD SKU Ambiguity**:
  - Update spec.md FR-003: "Virtual machine MUST use Standard HDD storage tier (Standard_LRS SKU) for the OS disk"
  - Update spec.md FR-008: "System MUST deploy an Azure Storage Account with HDD-backed file share (Standard_LRS SKU) with 1TB initial quota"
  - Update tasks.md T025: "Configure VM OS disk with Standard_LRS storage SKU (Standard HDD)"
  - Update tasks.md T030: "Configure VM data disks array with 500GB disk, Standard_LRS (Standard HDD)"
  - Update tasks.md T033: "Configure Storage Account with Standard_LRS SKU"

- **A2 - Disk Utilization Alert Threshold**:
  - Update spec.md FR-022: "System MUST configure critical alerts for VM stopped, OS disk used capacity >90% OR data disk used capacity >90%, and Key Vault access failures"
  - Update tasks.md T040: "Add Metric Alert Rule for OS disk used capacity >90% AND data disk used capacity >90% with separate alert conditions"

- **A3 - NSG Rules Incomplete**:
  - Update spec.md FR-020 to specify: "NSG MUST allow outbound traffic to AzureCloud service tag on port 443 (for Key Vault, Storage, Log Analytics); MUST allow inbound RDP (3389) from AzureBastionSubnet only; MUST deny all other inbound traffic from Internet"
  - Update tasks.md T012: "Add Network Security Group module with rules: Priority 100 Allow RDP inbound from AzureBastionSubnet, Priority 110 Allow HTTPS outbound to AzureCloud tag, Priority 4096 Deny all inbound from Internet"

- **A4 - Password Complexity Validation**:
  - Add assumption to spec.md Assumptions section: "Generated password formula '${uniqueString(resourceGroup().id)}${guid(subscription().id, resourceGroup().id)}A1!' produces compliant passwords meeting Azure VM requirements (12-123 characters, contains uppercase, lowercase, number, special character)"
  - OR add validation task between T013-T014: Verify password generation meets Azure complexity rules

- **A5 - AVM Module Versioning**:
  - Add research task to research.md: Query MCR tags for latest stable versions of all 10 AVM modules
  - Add versioning strategy to plan.md Technical Context: "Use exact version pinning (e.g., 0.10.2) for production deployments to ensure reproducibility"
  - Update all tasks T010-T041 to specify version format: `br/public:avm/res/<module>:0.x.x` (replace with actual versions)

### Recommended (MEDIUM Severity Issues - U1-U8)

These can be addressed during implementation but are recommended before finalizing:

- **U1**: Specify diagnostic log categories per resource type in research.md or assume defaults
- **U2**: Update spec.md US4 acceptance scenario 3 to replace "access policies" with "RBAC" for consistency
- **U3**: Add encryption-at-rest requirement or assumption (Azure default encryption assumed)
- **U4**: Add assumption to spec.md: "Alert notification actions (email, webhook, action groups) configured post-deployment"
- **U5**: Clarify in spec.md FR-016: "Availability zone MUST be user-selectable via parameter (values 1, 2, or 3)"
- **U6**: Document in quickstart.md: "Monitor file share usage via Azure Portal β†’ Storage Account β†’ File Shares β†’ Properties β†’ Quota"
- **U7**: Update tasks.md T028: "Enable VM boot diagnostics using managed storage (no separate storage account)"
- **U8**: Update tasks.md T009: "Create random suffix variable using substring(uniqueString(resourceGroup().id), 0, 6)"

### Optional (LOW Severity Issues - T1-T3)

Address during implementation or final polish:

- **T1**: Standardize terminology to "Storage Account" throughout all documents
- **T2**: Clarify in plan.md that environment parameter is for tagging only (resource group is deployment target)
- **T3**: Expand tasks.md T002 description: "Create infra/bicepconfig.json with AVM registry alias and analyzer rule use-recent-module-versions: warning"

---

## Deployment Readiness Assessment

### Overall Readiness: ⚠️ **CONDITIONALLY READY**

**Recommendation**: βœ… **SAFE TO PROCEED** with implementation **AFTER** resolving 5 HIGH severity issues (A1-A5)

**Justification**:
- βœ… **Strengths**:
  - Zero CRITICAL blockers
  - 100% constitution compliance
  - Complete requirements-to-task coverage
  - Well-structured user story organization with clear MVP definition
  - Comprehensive dependency analysis with parallelization opportunities
  - No unmapped tasks or orphaned requirements

- ⚠️ **Risks**:
  - HIGH issues create implementation ambiguity (developer may choose wrong SKU, incomplete NSG rules)
  - Missing AVM version specifications could lead to breaking changes or deployment failures
  - Underspecified thresholds (disk utilization) may trigger false alerts or miss real issues

- βœ… **Mitigation**:
  - All HIGH issues are resolvable through specification updates (no architecture changes required)
  - Estimated remediation time: 30-45 minutes
  - No blocking dependencies or conflicting requirements detected

### Pre-Implementation Checklist

Before running `/speckit.implement`, ensure:

- [ ] All 5 HIGH severity issues (A1-A5) resolved in spec.md and tasks.md
- [ ] Constitution compliance maintained (currently 100%)
- [ ] AVM module versions researched and documented
- [ ] NSG rules fully specified with priorities and service tags
- [ ] Azure VM password complexity requirements validated or assumed

### Implementation Risk Level: **LOW-MEDIUM**

With HIGH issues resolved, implementation risk drops to LOW. Current specification provides sufficient clarity for experienced Bicep developer but requires technical gap-filling that could introduce inconsistencies if not addressed upfront.

---

## Remediation Options

**Would you like concrete remediation edits for the 5 HIGH severity issues (A1-A5)?**

Options:
- **A. Yes, provide all edits** - Generate specific text replacements for spec.md, plan.md, tasks.md to resolve all HIGH issues
- **B. Only critical clarifications** - Provide edits for A1-A3 (SKU, alerts, NSG) only; defer A4-A5 for implementation phase
- **C. Manual remediation** - Keep analysis report as-is; I'll manually address issues based on recommendations

---

## Appendix: Document Statistics

### Specification Artifacts

| Document | Lines | Sections | Key Content |
|----------|-------|----------|-------------|
| constitution.md | 114 | 5 principles | Non-negotiable rules; 100% compliance |
| spec.md | 169 | 4 user stories, 24 FRs, 13 success criteria | Requirements definition; 2 clarifications |
| plan.md | 200 | Technical context, constitution checks, Phase 0-1 summary | Implementation blueprint; 6 design artifacts |
| data-model.md | 394 | 12 entities, ERD, deployment order | Resource definitions; dependency mapping |
| research.md | 236 | 8 research topics | AVM mapping, password strategy, network design |
| contracts/parameters.md | 317 | 11 parameters | Input contract; 3 required, 8 optional |
| contracts/outputs.md | 234 | 10 outputs | Output contract; administrative + operational |
| quickstart.md | ~300 | 7-step deployment guide | Deployment procedures; troubleshooting |
| tasks.md | 277 | 61 tasks across 7 phases | Implementation checklist; MVP + incremental delivery |

### Total Documentation Volume

- **Total Lines**: ~2,241 lines
- **Total Artifacts**: 9 files (+ checklists/implementation-readiness.md)
- **Estimated Reading Time**: 45-60 minutes
- **Estimated Implementation Time**: 8.5 hours (6 hours with parallelization)

---

**Report Generated**: 2026-01-26
**Next Action**: Resolve HIGH severity issues β†’ Run `/speckit.implement` β†’ Deploy to test environment

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your analysis report, with a comment of something like Analysis report generated.

8. Implement

Spec Kit uses /speckit.implement to generate the solution template file along with its parameters file. The prompt doesn’t require any specific inputs as it analyzes the existing plan and tasks to generate the implementation code.

Info

To learn more about the implement step, see the Implement chapter in the Spec Kit article.

  1. Run the following prompt to generate the implementation for our example:
/speckit.implement
βž• Expand to see the results

During the Implement phase, Copilot acts based on the tasks.md file (checkboxes next to each completed task get marked with [X]). It validates all previously created checklists, such as the implementation-readiness.md, requirements.md files. As a result of this prompt execution, a number of files get generated, such as:main.bicep, main.bicepparam, bicepconfig.json, .gitignore

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Specify Bootstrap
Specify Bootstrap
Specify Bootstrap
metadata name = 'Legacy VM Workload Infrastructure'
metadata description = 'Bicep template for deploying a legacy Windows Server 2016 VM workload with secure access, storage, and monitoring'
metadata owner = 'Infrastructure Team'

targetScope = 'resourceGroup'

// ============================================================================
// PARAMETERS
// ============================================================================

@description('Azure region for resource deployment')
param location string = 'westus3'

@description('Virtual machine size (SKU)')
param vmSize string = 'Standard_D2s_v3'

@description('VM administrator username')
@minLength(1)
@maxLength(20)
param vmAdminUsername string = 'vmadmin'

@description('Key Vault secret name for VM administrator password')
param vmAdminPasswordSecretName string = 'vm-admin-password'

@description('Availability zone for zone-capable resources (1, 2, or 3)')
@allowed([1, 2, 3])
param availabilityZone int = 1

@description('File share quota in GiB')
@minValue(100)
@maxValue(102400)
param fileShareQuotaGiB int = 1024

@description('Log Analytics workspace data retention in days')
@minValue(30)
@maxValue(730)
param logAnalyticsRetentionDays int = 30

@description('Deployment timestamp for password generation uniqueness')
param deploymentTime string = utcNow('u')

// ============================================================================
// VARIABLES
// ============================================================================

// Generate unique suffix for resource naming (6 characters from resource group ID)
var suffix = substring(uniqueString(resourceGroup().id), 0, 6)

// Generate VM administrator password using multiple seeds for uniqueness
// NOTE: Using deploymentTime parameter makes deployment non-idempotent - password regenerates each deploy.
// Acceptable for initial deployment; remove deploymentTime parameter for idempotent redeployments
var vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, deploymentTime)}'

// Resource naming following pattern: {resourceType}-{purpose}-{randomSuffix}
var vnetName = 'vnet-legacyvm-${suffix}'
var vmName = 'vm-legacyvm-${suffix}'
var kvName = 'kv-legacyvm-${suffix}'
var lawName = 'law-legacyvm-${suffix}'
// Storage account: no hyphens, lowercase only, max 24 chars
var stName = 'st${replace(suffix, '-', '')}'
var bastionName = 'bas-legacyvm-${suffix}'
var natGatewayName = 'nat-legacyvm-${suffix}'

// NSG names for each subnet
var nsgVmName = 'nsg-vm-legacyvm-${suffix}'
var nsgBastionName = 'nsg-bastion-legacyvm-${suffix}'
var nsgPeName = 'nsg-pe-legacyvm-${suffix}'

// Private DNS zone and endpoint names
var privateDnsZoneName = 'privatelink.file.${environment().suffixes.storage}'
var privateEndpointName = 'pe-file-legacyvm-${suffix}'

// Alert names
var alertVmStoppedName = 'alert-vm-stopped-legacyvm-${suffix}'
var alertDiskSpaceName = 'alert-disk-space-legacyvm-${suffix}'
var alertKvFailureName = 'alert-kv-access-fail-legacyvm-${suffix}'

// VM computer name (NetBIOS limit: 15 characters max)
var computerName = 'vm-${substring(suffix, 0, 6)}'

// Network configuration
var vnetAddressPrefix = '10.0.0.0/24'
var subnetVmAddressPrefix = '10.0.0.0/27'
var subnetBastionAddressPrefix = '10.0.0.64/26'
var subnetPeAddressPrefix = '10.0.0.128/27'

// Tags for all resources
var tags = {
  workload: 'legacy-vm'
  environment: 'production'
  compliance: 'legacy-retention'
  managedBy: 'bicep-avm'
}

// ============================================================================
// PHASE 2: FOUNDATIONAL RESOURCES
// ============================================================================

// Log Analytics Workspace - centralized logging for all resources
module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.15.0' = {
  name: 'deploy-log-analytics'
  params: {
    name: lawName
    location: location
    dataRetention: logAnalyticsRetentionDays
    tags: tags
  }
}

// ============================================================================
// PHASE 3: USER STORY 1 - CORE VM INFRASTRUCTURE
// ============================================================================

// Virtual Network with 3 subnets (VM, Bastion, Private Endpoint)
module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.2' = {
  name: 'deploy-vnet'
  params: {
    name: vnetName
    location: location
    addressPrefixes: [vnetAddressPrefix]
    subnets: [
      {
        name: 'snet-vm-legacyvm-${suffix}'
        addressPrefix: subnetVmAddressPrefix
        networkSecurityGroupResourceId: nsgVm.outputs.resourceId
        natGatewayResourceId: natGateway.outputs.resourceId
      }
      {
        name: 'AzureBastionSubnet' // Required name for Bastion
        addressPrefix: subnetBastionAddressPrefix
        networkSecurityGroupResourceId: nsgBastion.outputs.resourceId
      }
      {
        name: 'snet-pe-legacyvm-${suffix}'
        addressPrefix: subnetPeAddressPrefix
        networkSecurityGroupResourceId: nsgPe.outputs.resourceId
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
        metricCategories: [
          {
            category: 'AllMetrics'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Virtual Machine - Windows Server 2016
module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.21.0' = {
  name: 'deploy-vm'
  params: {
    name: vmName
    location: location
    computerName: computerName
    vmSize: vmSize
    availabilityZone: availabilityZone
    osType: 'Windows'
    imageReference: {
      publisher: 'MicrosoftWindowsServer'
      offer: 'WindowsServer'
      sku: '2016-Datacenter'
      version: 'latest'
    }
    osDisk: {
      createOption: 'FromImage'
      deleteOption: 'Delete'
      diskSizeGB: 127
      managedDisk: {
        storageAccountType: 'Standard_LRS' // HDD performance tier
      }
    }
    dataDisks: [
      {
        name: '${vmName}-datadisk-01'
        diskSizeGB: 500
        lun: 0
        createOption: 'Empty'
        managedDisk: {
          storageAccountType: 'Standard_LRS' // HDD performance tier
        }
      }
    ]
    adminUsername: vmAdminUsername
    adminPassword: vmPassword
    managedIdentities: {
      systemAssigned: true
    }
    nicConfigurations: [
      {
        name: '${vmName}-nic'
        ipConfigurations: [
          {
            name: 'ipconfig1'
            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0]
            privateIPAllocationMethod: 'Dynamic'
          }
        ]
      }
    ]
    bootDiagnostics: true
    tags: tags
  }
}

// ============================================================================
// PHASE 4: USER STORY 2 - SECURE STORAGE AND DATA DISK
// ============================================================================

// Storage Account with file share
module storageAccount 'br/public:avm/res/storage/storage-account:0.31.0' = {
  name: 'deploy-storage'
  params: {
    name: stName
    location: location
    kind: 'StorageV2'
    skuName: 'Standard_LRS'
    accessTier: 'Hot'
    publicNetworkAccess: 'Disabled'
    minimumTlsVersion: 'TLS1_2'
    fileServices: {
      shares: [
        {
          name: 'fileshare'
          shareQuota: fileShareQuotaGiB
          accessTier: 'TransactionOptimized'
        }
      ]
      diagnosticSettings: [
        {
          workspaceResourceId: logAnalytics.outputs.resourceId
          metricCategories: [
            {
              category: 'Transaction'
            }
          ]
        }
      ]
    }
    tags: tags
  }
}

// Private DNS Zone for storage private endpoint
module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = {
  name: 'deploy-private-dns-zone'
  params: {
    name: privateDnsZoneName
    location: 'global'
    virtualNetworkLinks: [
      {
        name: '${vnetName}-link'
        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
      }
    ]
    tags: tags
  }
}

// Private Endpoint for storage account file share
module privateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.1' = {
  name: 'deploy-private-endpoint'
  params: {
    name: privateEndpointName
    location: location
    subnetResourceId: virtualNetwork.outputs.subnetResourceIds[2] // PE subnet
    privateLinkServiceConnections: [
      {
        name: '${privateEndpointName}-connection'
        properties: {
          privateLinkServiceId: storageAccount.outputs.resourceId
          groupIds: ['file']
        }
      }
    ]
    privateDnsZoneGroup: {
      privateDnsZoneGroupConfigs: [
        {
          privateDnsZoneResourceId: privateDnsZone.outputs.resourceId
        }
      ]
    }
    tags: tags
  }
}

// ============================================================================
// PHASE 5: USER STORY 3 - SECURE ACCESS AND SECRETS MANAGEMENT
// ============================================================================

// Key Vault for storing VM admin password (created first, VM identity assigned later)
module keyVault 'br/public:avm/res/key-vault/vault:0.13.3' = {
  name: 'deploy-keyvault'
  params: {
    name: kvName
    location: location
    sku: 'standard'
    enableRbacAuthorization: true
    softDeleteRetentionInDays: 90
    enablePurgeProtection: false // Not required for legacy workload
    publicNetworkAccess: 'Enabled' // Simplified for legacy workload
    secrets: [
      {
        name: vmAdminPasswordSecretName
        value: vmPassword
        contentType: 'text/plain'
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
        metricCategories: [
          {
            category: 'AllMetrics'
          }
        ]
      }
    ]
    tags: tags
  }
}

// RBAC Assignment: Grant VM managed identity access to Key Vault secrets
// Note: Using guid with static strings (names) instead of outputs for deployment-time calculation
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(kvName, vmName, 'Key Vault Secrets User')
  scope: resourceGroup()
  properties: {
    principalId: virtualMachine.outputs.?systemAssignedMIPrincipalId!
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalType: 'ServicePrincipal'
  }
}

// Azure Bastion for secure RDP access
module bastion 'br/public:avm/res/network/bastion-host:0.8.2' = {
  name: 'deploy-bastion'
  params: {
    name: bastionName
    location: location
    skuName: 'Basic'
    virtualNetworkResourceId: virtualNetwork.outputs.resourceId
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// ============================================================================
// PHASE 6: USER STORY 4 - INTERNET CONNECTIVITY AND NETWORK SECURITY
// ============================================================================

// NAT Gateway for outbound internet connectivity
module natGateway 'br/public:avm/res/network/nat-gateway:2.0.1' = {
  name: 'deploy-nat-gateway'
  params: {
    name: natGatewayName
    location: location
    availabilityZone: availabilityZone
    publicIPAddresses: [
      {
        name: 'pip-nat-legacyvm-${suffix}'
      }
    ]
    tags: tags
  }
}

// Network Security Group for VM subnet
module nsgVm 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-vm'
  params: {
    name: nsgVmName
    location: location
    securityRules: [
      {
        name: 'AllowBastionRdpInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: subnetBastionAddressPrefix
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '3389'
        }
      }
      {
        name: 'DenyAllInbound'
        properties: {
          priority: 4096
          direction: 'Inbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowInternetOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'Internet'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowVnetOutbound'
        properties: {
          priority: 200
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRange: '*'
        }
      }
      {
        name: 'DenyAllOutbound'
        properties: {
          priority: 4096
          direction: 'Outbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Network Security Group for Bastion subnet
module nsgBastion 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-bastion'
  params: {
    name: nsgBastionName
    location: location
    securityRules: [
      // Inbound rules
      {
        name: 'AllowHttpsInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'Internet'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowGatewayManagerInbound'
        properties: {
          priority: 110
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'GatewayManager'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowAzureLoadBalancerInbound'
        properties: {
          priority: 120
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'AzureLoadBalancer'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowBastionHostCommunication'
        properties: {
          priority: 130
          direction: 'Inbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: 'VirtualNetwork'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['8080', '5701']
        }
      }
      // Outbound rules
      {
        name: 'AllowSshRdpOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['22', '3389']
        }
      }
      {
        name: 'AllowAzureCloudOutbound'
        properties: {
          priority: 110
          direction: 'Outbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'AzureCloud'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowBastionCommunication'
        properties: {
          priority: 120
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: 'VirtualNetwork'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['8080', '5701']
        }
      }
      {
        name: 'AllowGetSessionInformation'
        properties: {
          priority: 130
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'Internet'
          destinationPortRange: '80'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Network Security Group for Private Endpoint subnet
module nsgPe 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-pe'
  params: {
    name: nsgPeName
    location: location
    securityRules: [
      {
        name: 'AllowVMSubnetInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: subnetVmAddressPrefix
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '445'
        }
      }
      {
        name: 'DenyAllInbound'
        properties: {
          priority: 4096
          direction: 'Inbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowAllOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// ============================================================================
// PHASE 7: USER STORY 5 - MONITORING AND ALERTING
// ============================================================================

// Alert: VM Stopped/Deallocated
module alertVmStopped 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-vm-stopped'
  params: {
    name: alertVmStoppedName
    location: 'global'
    targetResourceType: 'Microsoft.Compute/virtualMachines'
    targetResourceRegion: location
    scopes: [
      virtualMachine.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'cpu-low-threshold'
          metricName: 'Percentage CPU'
          metricNamespace: 'Microsoft.Compute/virtualMachines'
          operator: 'LessThan'
          threshold: 1
          timeAggregation: 'Average'
        }
      ]
    }
    windowSize: 'PT15M'
    evaluationFrequency: 'PT5M'
    severity: 0
    enabled: true
    autoMitigate: false
    alertDescription: 'Critical: VM appears to be stopped or deallocated'
    tags: tags
  }
}

// Alert: Disk Space Exceeded 85%
module alertDiskSpace 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-disk-space'
  params: {
    name: alertDiskSpaceName
    location: 'global'
    targetResourceType: 'Microsoft.Compute/virtualMachines'
    targetResourceRegion: location
    scopes: [
      virtualMachine.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'disk-space-high-threshold'
          metricName: 'Available Memory Bytes'
          metricNamespace: 'Microsoft.Compute/virtualMachines'
          operator: 'LessThan'
          threshold: 1073741824 // 1GB in bytes
          timeAggregation: 'Average'
        }
      ]
    }
    windowSize: 'PT5M'
    evaluationFrequency: 'PT1M'
    severity: 0
    enabled: true
    autoMitigate: false
    alertDescription: 'Critical: Available memory below 1GB threshold'
    tags: tags
  }
}

// Alert: Key Vault Access Failures
module alertKvFailure 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-kv-failure'
  params: {
    name: alertKvFailureName
    location: 'global'
    targetResourceType: 'Microsoft.KeyVault/vaults'
    targetResourceRegion: location
    scopes: [
      keyVault.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'kv-access-failure-threshold'
          metricName: 'ServiceApiResult'
          metricNamespace: 'Microsoft.KeyVault/vaults'
          operator: 'GreaterThan'
          threshold: 0
          timeAggregation: 'Count'
        }
      ]
    }
    windowSize: 'PT5M'
    evaluationFrequency: 'PT1M'
    severity: 1
    enabled: true
    autoMitigate: false
    alertDescription: 'Warning: Key Vault API result failures detected'
    tags: tags
  }
}

// ============================================================================
// OUTPUTS
// ============================================================================

@description('Resource group location')
output location string = location

@description('Virtual Network resource ID')
output vnetResourceId string = virtualNetwork.outputs.resourceId

@description('Virtual Network name')
output vnetName string = virtualNetwork.outputs.name

@description('VM subnet resource ID')
output vmSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[0]

@description('Bastion subnet resource ID')
output bastionSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[1]

@description('Private Endpoint subnet resource ID')
output peSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[2]

@description('Virtual Machine resource ID')
output vmResourceId string = virtualMachine.outputs.resourceId

@description('Virtual Machine name')
output vmName string = virtualMachine.outputs.name

@description('VM private IP address - Note: Available after deployment, query via Azure Portal or CLI')
output vmPrivateIP string = ''

@description('VM system-assigned managed identity principal ID')
output vmManagedIdentityPrincipalId string = virtualMachine.outputs.?systemAssignedMIPrincipalId ?? ''

@description('Key Vault resource ID')
output keyVaultResourceId string = keyVault.outputs.?resourceId ?? ''

@description('Key Vault name')
output keyVaultName string = keyVault.outputs.?name ?? ''

@description('Storage Account resource ID')
output storageAccountResourceId string = storageAccount.outputs.resourceId

@description('Storage Account name')
output storageAccountName string = storageAccount.outputs.name

@description('File share name')
output fileShareName string = 'fileshare'

@description('Private Endpoint resource ID')
output privateEndpointResourceId string = privateEndpoint.outputs.resourceId

@description('Azure Bastion resource ID')
output bastionResourceId string = bastion.outputs.resourceId

@description('Azure Bastion name')
output bastionName string = bastion.outputs.name

@description('NAT Gateway resource ID')
output natGatewayResourceId string = natGateway.outputs.resourceId

@description('NAT Gateway name')
output natGatewayName string = natGateway.outputs.name

@description('Log Analytics Workspace resource ID')
output logAnalyticsResourceId string = logAnalytics.outputs.resourceId

@description('Log Analytics Workspace name')
output logAnalyticsName string = logAnalytics.outputs.name

@description('VM Stopped Alert resource ID')
output alertVmStoppedResourceId string = alertVmStopped.outputs.resourceId

@description('Disk Space Alert resource ID')
output alertDiskSpaceResourceId string = alertDiskSpace.outputs.resourceId

@description('Key Vault Access Failure Alert resource ID')
output alertKvFailureResourceId string = alertKvFailure.outputs.resourceId
using './main.bicep'

// ============================================================================
// DEPLOYMENT PARAMETERS
// ============================================================================

// Azure region for resource deployment
param location = 'westus3'

// Virtual machine configuration
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param vmAdminPasswordSecretName = 'vm-admin-password'
param availabilityZone = 1

// Storage configuration
param fileShareQuotaGiB = 1024

// Monitoring configuration
param logAnalyticsRetentionDays = 30
Specify Bootstrap

Coming soon!

  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your implementation results, with a comment of something like Implementation complete.

Next Steps

Congratulations! You’ve walked through a complete Spec Kit workflow for building Azure infrastructure using Azure Verified Modules. By following this structured approach, you’ve created a deployable IaC solution that is:

  • Well-documented: Every design decision is captured in the specification and plan.
  • Secure by default: The constitution enforces security baselines from the start.
  • Reproducible: The generated IaC template can be deployed consistently across environments.
  • Maintainable: Clear task breakdowns and checklists make future updates straightforward.

From here, you can ask Copilot to help you with the deployment and further enhancements, or you can manually take the following steps to deploy and manage your solution:

  1. Validate with What-If: Run az deployment group what-if to preview changes before deployment.
  2. Deploy to Azure: Use the Azure CLI or Bicep CLI to deploy your generated main.bicep to a subscription:
    az deployment group create \
      --resource-group <your-resource-group> \
      --template-file main.bicep \
      --parameters main.bicepparam
  3. Integrate into CI/CD: Add the generated templates to your Azure DevOps or GitHub Actions pipelines.
  4. Extend the solution: Iterate on the specification to add new capabilities while maintaining alignment with your constitution.

Coming soon!

For more information on Spec Kit and the underlying methodology, see the Spec Kit overview or explore the Specification-Driven Development concepts.