From 40e143fa4309ec607dd26a7f4922bffab500f359 Mon Sep 17 00:00:00 2001 From: Kris Turner Date: Tue, 17 Mar 2026 09:56:18 -0400 Subject: [PATCH 1/2] docs: consolidate variable docs to single reference Rewrite reference/variables.md with full sectioned YAML format matching config/variables.example.yml. Add naming rules, Key Vault resolution, and tool-specific mapping sections. Replace standards page with redirect. Part of AzureLocal/azurelocal.github.io#15 --- docs/reference/variables.md | 261 ++++++++++++++++++++++++++++++------ docs/standards/variables.md | 85 +----------- 2 files changed, 224 insertions(+), 122 deletions(-) diff --git a/docs/reference/variables.md b/docs/reference/variables.md index f82b015..e42e07a 100644 --- a/docs/reference/variables.md +++ b/docs/reference/variables.md @@ -1,66 +1,239 @@ # Variable Reference -All deployment parameters consolidated from every tool's parameter file. +All deployment tools read from a single central configuration file: `config/variables.yml`. This file is the **single source of truth** — your architecture decisions, sizing, identity settings, and infrastructure IDs are declared here and consumed by every automation tool. -See `config/variables.example.yml` for the full reference including types, defaults, and which phases consume each variable. +!!! tip "Getting started" + Copy the example and fill in your values: + ```powershell + cp config/variables.example.yml config/variables.yml + ``` + **Never commit** `variables.yml` — it is excluded by `.gitignore` because it contains environment-specific values and Key Vault references. -!!! warning "Secrets" - Never store passwords, tokens, or certificates in parameter files. Use Azure Key Vault references instead. +--- + +## Naming Rules + +| Scope | Convention | Example | +|-------|-----------|---------| +| Top-level sections | `snake_case` | `control_plane`, `session_hosts` | +| Keys within sections | `snake_case` | `subscription_id`, `vm_memory_mb` | +| Booleans | Descriptive name | `enable_entra_id_auth: true` | +| Secrets | `keyvault://` URI | `keyvault://kv-name/secret-name` | +| Example values | IIC fictional identity | `iic.local`, `rg-iic-avd-hp-eus-01`, `kv-iic-platform` | + +--- ## Subscription & Global -| Variable | Type | Default | -|----------|------|---------| -| `azure_subscription_id` | string | — | -| `azure_tenant_id` | string | — | -| `location` | string | `eastus` | -| `resource_group_name` | string | `rg-avd-prod` | +```yaml +subscription: + avd_subscription_id: "00000000-0000-0000-0000-000000000000" + azure_local_subscription_id: "00000000-0000-0000-0000-000000000000" + tenant_id: "00000000-0000-0000-0000-000000000000" + location: "eastus" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `subscription.avd_subscription_id` | string | **Yes** | Azure subscription for AVD control plane and session hosts | — | +| `subscription.azure_local_subscription_id` | string | **Yes** | Azure subscription where the Azure Local cluster is registered | — | +| `subscription.tenant_id` | string | **Yes** | Entra ID tenant | — | +| `subscription.location` | string | **Yes** | Azure region | `eastus` | + +--- + +## Security — Key Vault + +```yaml +security: + key_vault_name: "kv-iic-platform" + key_vault_resource_group: "rg-iic-mgmt-eus-01" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `security.key_vault_name` | string | **Yes** | Platform Key Vault for all `keyvault://` URI resolution | — | +| `security.key_vault_resource_group` | string | **Yes** | Resource group containing the Key Vault | — | + +--- ## Control Plane -| Variable | Type | Default | -|----------|------|---------| -| `host_pool_name` | string | `hp-azurelocal-pool01` | -| `host_pool_type` | string | `Pooled` | -| `load_balancer_type` | string | `BreadthFirst` | -| `max_session_limit` | int | `10` | -| `app_group_name` | string | `ag-avd-desktops` | -| `app_group_type` | string | `Desktop` | -| `workspace_name` | string | `ws-avd-prod` | -| `key_vault_name` | string | `kv-avd-prod-001` | -| `log_analytics_workspace_name` | string | `law-avd-prod` | -| `log_analytics_retention_days` | int | `30` | +```yaml +control_plane: + resource_group: "rg-iic-avd-hp-eus-01" + host_pool_name: "hp-iic-avd-pool01" + host_pool_type: "Pooled" + load_balancer_type: "BreadthFirst" + max_session_limit: 16 + preferred_app_group_type: "Desktop" + personal_assignment_type: "Automatic" + start_vm_on_connect: false + validation_environment: false + custom_rdp_properties: "" + app_group_name: "vdag-iic-avd-eus-01" + app_group_type: "Desktop" + workspace_name: "vdws-iic-avd-eus-01" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `control_plane.resource_group` | string | **Yes** | Resource group for host pool, app group, workspace | — | +| `control_plane.host_pool_name` | string | **Yes** | Host pool name | — | +| `control_plane.host_pool_type` | string | **Yes** | `Pooled` or `Personal` | `Pooled` | +| `control_plane.load_balancer_type` | string | Pooled | `BreadthFirst` or `DepthFirst` | `BreadthFirst` | +| `control_plane.max_session_limit` | integer | Pooled | Max concurrent sessions per host | `16` | +| `control_plane.preferred_app_group_type` | string | **Yes** | `Desktop`, `RailApplications`, or `None` | `Desktop` | +| `control_plane.personal_assignment_type` | string | Personal | `Automatic` or `Direct` | `Automatic` | +| `control_plane.start_vm_on_connect` | boolean | No | Requires Desktop Virtualization Power On Contributor RBAC | `false` | +| `control_plane.validation_environment` | boolean | No | Receives service updates before production | `false` | +| `control_plane.custom_rdp_properties` | string | No | Semicolon-delimited RDP properties | `""` | +| `control_plane.app_group_name` | string | **Yes** | Application group name | — | +| `control_plane.app_group_type` | string | **Yes** | `Desktop` or `RemoteApp` | `Desktop` | +| `control_plane.workspace_name` | string | **Yes** | AVD workspace name | — | + +--- ## Session Hosts -| Variable | Type | Default | -|----------|------|---------| -| `custom_location_id` | string | — | -| `vm_name_prefix` | string | `avd-sh` | -| `vm_count` | int | `2` | -| `vm_size` | string | `Standard_D4s_v3` | -| `image_id` | string | — | -| `vnet_id` | string | — | -| `subnet_name` | string | `default` | -| `key_vault_id` | string | — | +```yaml +session_hosts: + resource_group: "rg-iic-avd-sh-eus-01" + session_host_count: 2 + vm_naming_prefix: "vm-iicavd" + vm_start_index: 1 + vm_processors: 4 + vm_memory_mb: 16384 + vm_admin_username: "avd_admin" + vm_admin_password: "keyvault://kv-iic-platform/avd-local-admin-password" + session_host_os: "Windows-11-Enterprise-Multi-Session" + custom_location_id: "" + logical_network_id: "" + gallery_image_id: "" + storage_path_id: "" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `session_hosts.resource_group` | string | **Yes** | Resource group for session host VMs | — | +| `session_hosts.session_host_count` | integer | **Yes** | Number of session host VMs | `2` | +| `session_hosts.vm_naming_prefix` | string | **Yes** | VMs named `{prefix}-001`, `{prefix}-002`, etc. | `vm-iicavd` | +| `session_hosts.vm_start_index` | integer | No | Starting index for VM numbering | `1` | +| `session_hosts.vm_processors` | integer | **Yes** | vCPUs per session host | `4` | +| `session_hosts.vm_memory_mb` | integer | **Yes** | RAM per VM in MB | `16384` | +| `session_hosts.vm_admin_username` | string | **Yes** | Local admin username | `avd_admin` | +| `session_hosts.vm_admin_password` | string | **Yes** | Key Vault URI — resolved at runtime | — | +| `session_hosts.session_host_os` | string | **Yes** | OS image name | `Windows-11-Enterprise-Multi-Session` | +| `session_hosts.custom_location_id` | string | **Yes** | Azure Local custom location resource ID | — | +| `session_hosts.logical_network_id` | string | **Yes** | Compute logical network resource ID | — | +| `session_hosts.gallery_image_id` | string | **Yes** | Gallery image resource ID | — | +| `session_hosts.storage_path_id` | string | **Yes** | Storage path resource ID | — | + +--- ## Domain Join -| Variable | Type | Default | -|----------|------|---------| -| `domain_fqdn` | string | `iic.local` | -| `domain_join_user` | string | — | -| `ou_path` | string | — | +```yaml +domain: + domain_fqdn: "iic.local" + domain_join_username: "svc.domainjoin" + domain_join_password: "keyvault://kv-iic-platform/domain-join-password" + domain_join_ou_path: "" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `domain.domain_fqdn` | string | **Yes** | Active Directory domain FQDN | `iic.local` | +| `domain.domain_join_username` | string | **Yes** | Service account for domain join | — | +| `domain.domain_join_password` | string | **Yes** | Key Vault URI for domain join password | — | +| `domain.domain_join_ou_path` | string | No | Target OU — empty uses default Computers container | `""` | + +--- + +## Entra ID Authentication + +```yaml +entra_id: + enable_entra_id_auth: false + enroll_in_intune: false + entra_user_login_group_id: "" + entra_admin_login_group_id: "" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `entra_id.enable_entra_id_auth` | boolean | No | Installs AADLoginForWindows extension + RDP SSO | `false` | +| `entra_id.enroll_in_intune` | boolean | No | Register session hosts in Intune MDM | `false` | +| `entra_id.entra_user_login_group_id` | string | No | Entra group object ID for VM User Login RBAC | `""` | +| `entra_id.entra_admin_login_group_id` | string | No | Entra group object ID for VM Administrator Login RBAC | `""` | + +--- ## Tags -| Variable | Type | Default | -|----------|------|---------| -| `environment_tag` | string | `production` | -| `owner_tag` | string | `platform-team` | +```yaml +tags: + Environment: "Production" + Project: "AVD on Azure Local" + ManagedBy: "Infrastructure as Code" + Owner: "Platform Team" + CostCenter: "IT-Infrastructure" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `tags.Environment` | string | No | Environment tag | `Production` | +| `tags.Project` | string | No | Project tag | `AVD on Azure Local` | +| `tags.ManagedBy` | string | No | Managed-by tag | `Infrastructure as Code` | +| `tags.Owner` | string | No | Owner tag | `Platform Team` | +| `tags.CostCenter` | string | No | Cost center tag | `IT-Infrastructure` | + +--- ## Ansible -| Variable | Type | Default | -|----------|------|---------| -| `ansible_connection` | string | `local` | +```yaml +ansible: + ansible_connection: "local" +``` + +| Variable | Type | Required | Description | Default | +|----------|------|:--------:|-------------|---------| +| `ansible.ansible_connection` | string | No | Ansible connection type | `local` | + +--- + +## Key Vault Secret Resolution + +Secrets are never stored in plaintext. The `keyvault://` URI format tells deployment tools to resolve the value at runtime: + +```yaml +vm_admin_password: "keyvault://kv-iic-platform/avd-local-admin-password" +``` + +**Resolution flow:** + +1. Tool parses the URI → vault name: `kv-iic-platform`, secret name: `avd-local-admin-password` +2. Tool calls `az keyvault secret show --vault-name kv-iic-platform --name avd-local-admin-password` +3. Secret value is passed directly to the deployment — never written to disk + +**Required secrets:** + +| Secret Name | Used By | +|------------|---------| +| `avd-local-admin-password` | Local admin password for session host VMs | +| `domain-join-password` | Service account password for domain join | + +--- + +## Tool-Specific Parameter Mapping + +Each automation tool reads from `config/variables.yml` and maps values to its own parameter format: + +| Tool | Parameter File | Location | +|------|---------------|----------| +| **PowerShell** | Reads `config/variables.yml` directly | `config/` | +| **Bicep** | `*.bicepparam` | `avd/bicep/` | +| **Terraform** | `terraform.tfvars` | `avd/terraform/` | +| **ARM** | `*.parameters.json` | `avd/arm/` | +| **Ansible** | `hosts.yml` | `src/ansible/inventory/` | diff --git a/docs/standards/variables.md b/docs/standards/variables.md index baf3936..7303dcc 100644 --- a/docs/standards/variables.md +++ b/docs/standards/variables.md @@ -1,82 +1,11 @@ # Variable Standards -Conventions for the central configuration file and all variable naming across the repository. +!!! info "This page has moved" + Variable standards, naming conventions, and the complete configuration reference are now consolidated at **[Variable Reference](../reference/variables.md)**. ---- +See [Variable Reference](../reference/variables.md) for: -## Central Config - -The single source of truth is `config/variables.yml`. Copy from `config/variables.example.yml`. - -### Format - -- **YAML** with sectioned structure -- Clean, human-readable keys (no legacy prefixes) -- 2-space indentation - -### Sections - -```yaml -subscription: # Azure subscriptions (AVD + Azure Local), tenant, location -security: # Key Vault name and resource group -control_plane: # Host pool, app group, workspace settings -session_hosts: # VM count, prefix, specs, admin creds, Azure Local resource IDs -domain: # AD domain config, join credentials, OU paths -entra_id: # Entra ID SSO, Intune enrollment, RBAC group IDs -tags: # Resource tags -ansible: # Ansible controller details (optional) -``` - -### Naming Rules - -| Scope | Convention | Example | -|-------|-----------|---------| -| Top-level sections | `snake_case` | `azure_local`, `data_disks` | -| Keys within sections | `snake_case` | `subscription_id`, `volume_size_gb` | -| Per-VM maps | Zero-padded string keys | `"01"`, `"02"`, `"03"` | -| Booleans | Descriptive name | `role_enabled: true` | -| Secrets | `keyvault://` URI | `keyvault://kv-name/secret-name` | -| Example values | IIC fictional identity | `iic.local`, `rg-iic-avd-hp-eus-01`, `kv-iic-platform` | - ---- - -## Key Vault References - -Secrets are **never** stored as plain text in config files. Use the `keyvault://` URI format: - -```yaml -session_hosts: - vm_admin_password: "keyvault://kv-iic-platform/avd-vm-admin-password" -domain: - domain_join_password: "keyvault://kv-iic-platform/domain-join-password" -``` - -At runtime, scripts resolve these via `Resolve-KeyVaultRef`: - -1. Try `Az.KeyVault` PowerShell module (preferred) -2. Fallback to `az keyvault secret show` CLI -3. Hard fail if neither works (no interactive prompts) - ---- - -## Compatibility - -The deploy scripts read the central `config/variables.yml` directly. Legacy config file layouts (e.g., `solution-avd.yml`) are **not** supported — migrate to the new section-based structure. - -- New `config/variables.yml` → required by all deploy scripts -- See `config/variables.example.yml` for a complete reference - ---- - -## Tool-Specific Parameter Files - -| Tool | File | Location | -|------|------|----------| -| PowerShell | `variables.yml` | `config/` | -| Bicep | `*.bicepparam` | `src/bicep/` | -| Terraform | `terraform.tfvars` | `src/terraform/` | -| ARM | `*.parameters.json` | `src/arm/` | -| Ansible | `hosts.yml` | `src/ansible/inventory/` | -| Azure CLI | `parameters.env` | `scripts/` | - -All tool-specific parameter files should derive their values from the central `config/variables.yml`. +- Naming rules and conventions +- Every configuration section with types, defaults, and descriptions +- Key Vault secret resolution +- Tool-specific parameter mapping From b12567a490308c7193f104d65c85977d623720f7 Mon Sep 17 00:00:00 2001 From: Kris Turner Date: Tue, 17 Mar 2026 10:01:40 -0400 Subject: [PATCH 2/2] ci: add JSON Schema and validate-config workflow Add config/schema/variables.schema.json for validating variables.example.yml. Add CI workflow that validates example config against schema on PR. Part of AzureLocal/azurelocal.github.io#15 --- .github/workflows/validate-config.yml | 60 ++++++++++++++++ config/schema/variables.schema.json | 100 ++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 .github/workflows/validate-config.yml create mode 100644 config/schema/variables.schema.json diff --git a/.github/workflows/validate-config.yml b/.github/workflows/validate-config.yml new file mode 100644 index 0000000..538f0ef --- /dev/null +++ b/.github/workflows/validate-config.yml @@ -0,0 +1,60 @@ +# ============================================================================= +# validate-config.yml — Validate config/variables.example.yml against schema +# ============================================================================= +# Triggered on PRs and pushes that touch config/ or this workflow. +# Validates YAML syntax and JSON Schema compliance. +# ============================================================================= + +name: Validate Configuration + +on: + push: + branches: [main] + paths: + - 'config/**' + - '.github/workflows/validate-config.yml' + pull_request: + branches: [main] + paths: + - 'config/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install pyyaml jsonschema + + - name: Validate variables.example.yml against schema + run: | + python3 -c " + import yaml, json, sys + from jsonschema import validate, ValidationError + + with open('config/variables.example.yml') as f: + data = yaml.safe_load(f) + + with open('config/schema/variables.schema.json') as f: + schema = json.load(f) + + try: + validate(instance=data, schema=schema) + print('✅ config/variables.example.yml passes schema validation') + except ValidationError as e: + print(f'❌ Schema validation failed: {e.message}') + print(f' Path: {\" > \".join(str(p) for p in e.absolute_path)}') + sys.exit(1) + " diff --git a/config/schema/variables.schema.json b/config/schema/variables.schema.json new file mode 100644 index 0000000..76b1cf1 --- /dev/null +++ b/config/schema/variables.schema.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/AzureLocal/aurelocal-avd/config/schema/variables.schema.json", + "title": "AVD on Azure Local Variables", + "description": "Schema for config/variables.example.yml — validates required sections and key structure.", + "type": "object", + "required": ["subscription", "security", "control_plane", "session_hosts", "domain", "tags"], + "properties": { + "subscription": { + "type": "object", + "required": ["avd_subscription_id", "azure_local_subscription_id", "tenant_id", "location"], + "properties": { + "avd_subscription_id": { "type": "string" }, + "azure_local_subscription_id": { "type": "string" }, + "tenant_id": { "type": "string" }, + "location": { "type": "string" } + } + }, + "security": { + "type": "object", + "required": ["key_vault_name", "key_vault_resource_group"], + "properties": { + "key_vault_name": { "type": "string" }, + "key_vault_resource_group": { "type": "string" } + } + }, + "control_plane": { + "type": "object", + "required": ["resource_group", "host_pool_name", "host_pool_type", "app_group_name", "workspace_name"], + "properties": { + "resource_group": { "type": "string" }, + "host_pool_name": { "type": "string" }, + "host_pool_type": { "type": "string", "enum": ["Pooled", "Personal"] }, + "load_balancer_type": { "type": "string", "enum": ["BreadthFirst", "DepthFirst"] }, + "max_session_limit": { "type": "integer", "minimum": 1 }, + "preferred_app_group_type": { "type": "string", "enum": ["Desktop", "RailApplications", "None"] }, + "personal_assignment_type": { "type": "string", "enum": ["Automatic", "Direct"] }, + "start_vm_on_connect": { "type": "boolean" }, + "validation_environment": { "type": "boolean" }, + "custom_rdp_properties": { "type": "string" }, + "host_pool_friendly_name": { "type": "string" }, + "host_pool_description": { "type": "string" }, + "app_group_name": { "type": "string" }, + "app_group_type": { "type": "string", "enum": ["Desktop", "RemoteApp"] }, + "app_group_friendly_name": { "type": "string" }, + "workspace_name": { "type": "string" }, + "workspace_friendly_name": { "type": "string" } + } + }, + "session_hosts": { + "type": "object", + "required": ["resource_group", "session_host_count", "vm_naming_prefix", "vm_processors", "vm_memory_mb", "vm_admin_username", "vm_admin_password", "custom_location_id", "logical_network_id", "gallery_image_id", "storage_path_id"], + "properties": { + "resource_group": { "type": "string" }, + "session_host_count": { "type": "integer", "minimum": 1 }, + "vm_naming_prefix": { "type": "string" }, + "vm_start_index": { "type": "integer", "minimum": 0 }, + "vm_processors": { "type": "integer", "minimum": 1 }, + "vm_memory_mb": { "type": "integer", "minimum": 1024 }, + "vm_admin_username": { "type": "string" }, + "vm_admin_password": { "type": "string" }, + "session_host_os": { "type": "string" }, + "custom_location_id": { "type": "string" }, + "logical_network_id": { "type": "string" }, + "gallery_image_id": { "type": "string" }, + "storage_path_id": { "type": "string" } + } + }, + "domain": { + "type": "object", + "required": ["domain_fqdn", "domain_join_username", "domain_join_password"], + "properties": { + "domain_fqdn": { "type": "string" }, + "domain_join_username": { "type": "string" }, + "domain_join_password": { "type": "string" }, + "domain_join_ou_path": { "type": "string" } + } + }, + "entra_id": { + "type": "object", + "properties": { + "enable_entra_id_auth": { "type": "boolean" }, + "enroll_in_intune": { "type": "boolean" }, + "entra_user_login_group_id": { "type": "string" }, + "entra_admin_login_group_id": { "type": "string" } + } + }, + "tags": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "ansible": { + "type": "object", + "properties": { + "ansible_connection": { "type": "string" } + } + } + }, + "additionalProperties": false +}