Creates the Azure Virtual Desktop workspace — the feed a user's Remote Desktop client subscribes to. Targets
hashicorp/azurerm ~> 4.0.
- 🖥️ Manages
azurerm_virtual_desktop_workspace— the feed that turns application groups into something a person can see. - 🚫 Publishes nothing by itself. Attaching an application group is a separate resource.
- 🔒 Inverts the provider's public-access default to
false, and states exactly what that costs. - ✍️ Reports that
friendly_nameanddescriptionare always sent, so omitting one clears it. - 📏 Mirrors the provider's name rule exactly, including the asymmetry that a name may end in an underscore.
- 🌍 Names the confusion worth naming: the workspace's region is metadata, not where sessions run.
💡 Why it matters: a workspace is the one Azure Virtual Desktop object users actually interact with, and it is deceptively inert. It creates no compute, publishes nothing until an association exists, authorises nobody, and quietly clears its own display name if you omit the argument. This module makes each of those explicit rather than leaving them to be discovered.
If this module saved you time:
- ⭐ Star the repository — it is the cheapest signal that this work is worth continuing.
- 💼 Connect on LinkedIn — linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee — buymeacoffee.com/microsoftexpert
flowchart TB
RG["terraform-azurerm-resource-group"]
HP["terraform-azurerm-virtual-desktop-host-pool"]
AG["terraform-azurerm-virtual-desktop-application-group"]
WS["terraform-azurerm-virtual-desktop-workspace"]
ASSOC["azurerm_virtual_desktop_workspace_application_group_association"]
RA["terraform-azurerm-role-assignments"]
PE["terraform-azurerm-private-endpoint"]
RG -->|"name to resource_group_name"| WS
RG -->|"name to resource_group_name"| HP
HP -->|"id to host_pool_id"| AG
WS -->|"id to workspace_id"| ASSOC
AG -->|"id to application_group_id"| ASSOC
AG -->|"id to scope -- Desktop Virtualization User"| RA
WS -.->|"required when the feed is not public"| PE
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002B4A,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class WS self
class ASSOC keystone
class RG,HP,AG,RA,PE ext
The association in the centre is the keystone of the relationship, not of this module — it is a separate resource that joins a workspace to an application group, and neither side owns it. Read the dotted edge as a requirement rather than a data flow: with public access disabled, a private endpoint is what makes the feed reachable at all, and this module neither creates it nor can see it.
flowchart TB
V1["name -- 3-64, may end in an underscore"]
V2["resource_group_name and location"]
V3["friendly_name -- what users SEE"]
V4["description"]
V5["public_network_access_enabled -- INVERTED to false"]
V6["tags and timeouts"]
THIS["azurerm_virtual_desktop_workspace.this"]
O1["id -- consumed by the application group association"]
O2["private_link_is_required_for_the_feed"]
O3["feed_is_publicly_reachable"]
O4["posture -- publishes nothing by itself"]
V1 --> THIS
V2 --> THIS
V3 --> THIS
V4 --> THIS
V5 --> THIS
V6 --> THIS
THIS --> O1
THIS --> O2
THIS --> O3
THIS --> O4
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002B4A,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class THIS keystone
class O1 self
class V1,V2,V3,V4,V5,V6,O2,O3,O4 ext
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_virtual_desktop_workspace |
1 (this) |
A metadata record. It creates no compute and publishes nothing until an application group is associated with it. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module. The caller configures provider "azurerm" { features {} }, including authentication. |
Schema notes that bite — each verified against the live provider schema and the resource's own source:
- 🔴
friendly_nameanddescriptionare ALWAYS sent. The provider passes both on every write whether or not they are configured, so omitting one sends an empty string rather than leaving the current value alone. A workspace adopted by import and applied without them has its display name cleared. - 🔴 This module inverts
public_network_access_enabledtofalse. The provider defaults it totrue. The divergence follows this suite's secure-by-default rule and matches the already-authored host-pool module — but it is not free: see the Design Principles table. ⚠️ The name rule is invisible in the published schema — 3 to 64 characters, letters, digits, dots, dashes and underscores, beginning with a letter or digit and ending with a letter, digit or underscore. Sows_prod_is legal andws-prod-is not.⚠️ The host pool expresses the same access idea as a four-valued STRING —Disabled,EnabledForSessionHostsOnly,EnabledForClientsOnly,Enabled— where this resource uses a boolean.⚠️ The read treats any absent or non-Enabledvalue as public, so an older workspace whose property the service does not return reads back as public.- ℹ️ The workspace's region is metadata, not where sessions run.
- ℹ️ The create is a single synchronous call, not a polled long-running operation, despite the hour-long default timeouts.
- ℹ️ The delete takes a provider-internal lock on the workspace name.
- ℹ️ An import guard exists, and the resource carries a state upgrader (schema version 1 from 0).
- ℹ️ All four timeouts are honoured, and
locationis normalised.
Least privilege, at the smallest scope that works.
| Scope | Permission | Why |
|---|---|---|
| The resource group | Microsoft.DesktopVirtualization/workspaces/read |
Refresh and plan. |
| The resource group | Microsoft.DesktopVirtualization/workspaces/write |
Create and update. The workspace does not exist before the first apply, so the grant cannot be scoped to it. |
| The resource group | Microsoft.DesktopVirtualization/workspaces/delete |
Destroy. |
| — | Built-in fit: Desktop Virtualization Contributor at the resource group. | The least-privilege built-in that covers workspace management; Contributor grants far more. |
🔒 No credential of any kind is accepted or emitted by this module. A workspace carries no key, token or password.
ℹ️ Who may USE the feed is decided elsewhere. Access comes from Entra ID role assignments scoped to the application groups this workspace publishes — typically Desktop Virtualization User — and those are a separate resource.
- The
Microsoft.DesktopVirtualizationresource provider registered in the subscription. - Nothing else for the workspace itself. It is a metadata record and creates no compute.
- For the default
public_network_access_enabled = false: Azure Virtual Desktop Private Link — a private endpoint for this workspace's feed, a private endpoint for the host pool's connections, and DNS that resolves the AVD private zones from the client network. Without all three, users cannot retrieve their feed at all. - For anyone to see anything: at least one application group, an association joining it to this workspace, and a role assignment on that group.
terraform-azurerm-virtual-desktop-workspace/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # 8 inputs, 7 validations, deeply-typed with the schema in the descriptions
├── main.tf # one keystone `this`; dynamic timeouts
├── outputs.tf # 25 outputs: id first, then identity, then the posture facts
├── README.md # this file
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
provider "azurerm" {
features {}
}
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = "Production Desktops"
# This module defaults public access to FALSE. Set it true unless AVD
# Private Link is genuinely in place -- see example 3.
public_network_access_enabled = true
}
⚠️ The empty call is deliberately the private one. Readprivate_link_is_required_for_the_feedbefore accepting it.
ℹ️ The caller configures the provider, its authentication, and the mandatory
features {}block.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group → name |
location |
string |
terraform-azurerm-resource-group → location |
name, friendly_name, description, public_network_access_enabled, tags, timeouts |
caller |
Emits
| Output | Consumed by |
|---|---|
id |
the workspace-to-application-group association, private endpoints, diagnostics |
name, resource_group_name, location |
compositions |
feed_is_publicly_reachable, private_link_is_required_for_the_feed |
security and architecture review |
friendly_name_was_supplied, description_was_supplied |
drift review |
| the posture constants | human readers |
1 · The smallest real call
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
}
⚠️ This applies cleanly and shows nobody anything. A workspace is an empty feed until an application group is associated with it, and this call also leaves public access disabled.
2 · The ordinary deployment — a public feed
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = "Production Desktops"
description = "Finance and operations desktops."
public_network_access_enabled = true
}
output "posture" {
value = module.avd_workspace.feed_is_publicly_reachable # true
}💡 This is what Azure Virtual Desktop was designed for: users connect from anywhere over the internet, authenticated by Entra ID. The module makes you type it so that it reads as a decision.
3 · The private feed, and what it actually requires
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = "Production Desktops"
# public_network_access_enabled defaults to false
}
module "avd_feed_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-private-endpoint.git?ref=v1.0.0"
name = "pe-avd-feed"
resource_group_name = module.rg.name
location = module.rg.location
subnet_id = var.private_endpoint_subnet_id # from the virtual-network module
private_service_connection = {
name = "avd-feed"
private_connection_resource_id = module.avd_workspace.id
subresource_names = ["feed"]
is_manual_connection = false
}
}🔴 A private endpoint on the workspace covers the feed only. Connections to the desktops themselves go through a private endpoint on the host pool, and both need DNS that resolves the AVD private zones from the client network. Miss any of it and users see nothing at all —
private_link_is_required_for_the_feedis the module saying so.
4 · The display name that gets cleared
# Applied once, with a display name:
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = "Production Desktops"
}
# Applied again with the argument removed -- this does NOT leave it alone.
# The provider sends friendly_name on every write, so it is set to "".🔴 Omission is an instruction here, not an absence. It matters most when adopting a workspace by import: applying without these arguments clears whatever the portal set.
friendly_name_was_suppliedreports which way round you are.
5 · The name rule, exactly
name = "ws-prod" # fine
name = "ws_prod_" # fine -- a TRAILING UNDERSCORE is legal
name = "ws.prod-eastus_1" # fine -- dots, dashes and underscores inside
name = "ws1" # fine -- three characters is the floor
name = "ws" # rejected -- shorter than 3
name = "ws prod" # rejected -- no spaces
name = "_wsprod" # rejected -- must begin with a letter or digit
name = "wsprod-" # rejected -- a trailing dash is not a word character
name = "wsprod." # rejected -- nor is a trailing dot
⚠️ The asymmetry is the provider's, not this module's: the trailing check is a word-character test, which accepts an underscore. The application group's name rule is identical, character for character.
6 · Publishing an application group to this workspace
resource "azurerm_virtual_desktop_workspace_application_group_association" "desktop" {
workspace_id = module.avd_workspace.id
application_group_id = module.avd_desktop_group.id
}💡 This is the step that makes the feed non-empty, and it is deliberately not part of this module: a workspace publishes many groups, a group can move between workspaces, and folding the join in would make destroying a workspace take its groups' membership with it.
7 · Several workspaces from one map
locals {
workspaces = {
finance = { friendly_name = "Finance Desktops", description = "Finance line-of-business apps." }
ops = { friendly_name = "Operations Desktops", description = "Warehouse and logistics." }
}
}
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
for_each = local.workspaces
name = "ws-${each.key}"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = each.value.friendly_name
description = each.value.description
public_network_access_enabled = true
}ℹ️ Keying on a stable identifier rather than an index keeps
for_eachkeys stable when a workspace is added or removed.
8 · The region confusion, stated plainly
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = "eastus" # where the workspace METADATA lives
# ...session hosts may be in westus2, centralus, or anywhere else.
}
⚠️ This location decides where the workspace's own service objects are stored. Sessions run on session hosts, which are virtual machines in whatever region they were built in — commonly and legitimately a different one. It is a frequent source of confusion in cost and data-residency reviews.
9 · Reading the posture back
output "avd_feed" {
value = {
id = module.avd_workspace.id
public = module.avd_workspace.feed_is_publicly_reachable
needs_private = module.avd_workspace.private_link_is_required_for_the_feed
display_name = module.avd_workspace.friendly_name
named = module.avd_workspace.friendly_name_was_supplied
}
}🔒 The pair worth reading together is
publicandneeds_private: exactly one of them is true, and the second is the one that names a dependency this module cannot create.
10 · Tags, and what they do not cover
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
tags = { env = "prod", owner = "eus-platform", cost_centre = "4412" }
}ℹ️ These reach the workspace record and nothing else. The application groups, the host pools and the session hosts are separate ARM resources with their own tags, so a policy evaluating only this record says nothing about them.
11 · Timeouts
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "30m"
}
}ℹ️ All four are genuinely honoured — worth stating, because resources elsewhere in this library accept a timeout they then ignore, take the create deadline on an update, or have no update operation at all. The hour-long defaults are generous for what this does: the create is a single synchronous call. A misspelled key is discarded silently.
12 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-avd-eastus"
location = "eastus"
}
module "avd_host_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-host-pool.git?ref=v1.0.0"
name = "hp-prod"
resource_group_name = module.rg.name
location = module.rg.location
type = "Pooled"
load_balancer_type = "BreadthFirst"
# Must agree with the application group's `type` -- and note the spelling.
preferred_app_group_type = "Desktop"
}
module "avd_desktop_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-application-group.git?ref=v1.0.0"
name = "ag-desktop"
resource_group_name = module.rg.name
location = module.rg.location
type = "Desktop"
host_pool_id = module.avd_host_pool.id
friendly_name = "Production Desktop"
default_desktop_display_name = "Production Desktop"
}
module "avd_workspace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-desktop-workspace.git?ref=v1.0.0"
name = "ws-prod"
resource_group_name = module.rg.name
location = module.rg.location
friendly_name = "Production Desktops"
description = "Finance and operations desktops."
public_network_access_enabled = true
tags = { env = "prod" }
}
# The join that makes the feed non-empty.
resource "azurerm_virtual_desktop_workspace_application_group_association" "desktop" {
workspace_id = module.avd_workspace.id
application_group_id = module.avd_desktop_group.id
}
# And the grant that lets a user see it. Scoped to the GROUP, not the workspace.
module "avd_users" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.avd_desktop_group.id
role_assignments = {
desktop_users = {
principal_id = var.avd_users_group_object_id
role_definition_name = "Desktop Virtualization User"
principal_type = "Group"
description = "Lets the finance team sign in to the production desktop."
}
}
}
output "avd" {
value = {
workspace = module.avd_workspace.id
public_feed = module.avd_workspace.feed_is_publicly_reachable
group = module.avd_desktop_group.id
pool_type_set = module.avd_desktop_group.matching_host_pool_preferred_app_group_type
}
}🔒 Every reference is a real output of a real sibling, and the shape is the point: four things must exist before a person can launch a desktop — a host pool, an application group, a workspace, an association — and then a role assignment on the group. This module is one of them.
Identity — name (3–64, force-new), resource_group_name, location (both force-new)
Display — friendly_name, description (both always sent)
Access — public_network_access_enabled (inverted to false)
Tail — tags, timeouts
Full input schemas
variable "name" { type = string }
# 3-64 characters; letters, digits, dots, dashes, underscores.
# Must BEGIN with a letter or digit and END with a letter, digit or UNDERSCORE.
# FORCE-NEW.
variable "resource_group_name" { type = string } # FORCE-NEW
variable "location" { type = string } # FORCE-NEW, normalised by the provider
variable "friendly_name" { type = string, default = null } # 1-64; ALWAYS SENT
variable "description" { type = string, default = null } # 1-512; ALWAYS SENT
variable "public_network_access_enabled" {
type = bool
default = false # INVERTS the provider's `true` -- see the Design Principles table
}
variable "tags" { type = map(string), default = {} }
variable "timeouts" { type = object({ create, read, update, delete }), default = null }| Output | Description | Notes |
|---|---|---|
id |
Resource ID of the workspace. | Consumed by the association. |
name |
The Azure resource name. | Not what users see. |
resource_group_name, location |
Identity. | Location is normalised. |
friendly_name |
What users see. | Empty when none was configured. |
description |
Free text. | |
public_network_access_enabled |
As the provider reports it. | |
tags |
On this record only. | |
feed_is_publicly_reachable |
Derived. | Conditional. |
private_link_is_required_for_the_feed |
The cost of this module's default. | Conditional. |
friendly_name_was_supplied |
Because omission is not absence. | Conditional. |
description_was_supplied |
Same. | Conditional. |
this_workspace_publishes_nothing_by_itself |
Constant true. | The first thing to understand. |
the_public_access_default_is_inverted_from_the_provider |
Constant true. | Stated so it never surprises. |
friendly_name_and_description_are_always_sent |
Constant true. | Changes what omission means. |
the_workspace_region_is_metadata_not_where_sessions_run |
Constant true. | |
the_host_pool_expresses_public_access_as_a_four_valued_string |
Constant true. | Intra-family asymmetry. |
the_name_may_end_in_an_underscore_but_not_a_dot_or_dash |
Constant true. | |
an_import_guard_exists_on_this_resource |
Constant true. | Import, do not apply over. |
this_resource_carries_a_state_upgrader |
Constant true. | |
all_four_timeouts_are_honoured_here |
Constant true. | |
force_new_fields, fields_that_can_change_after_creation, fields_azure_returns_on_read |
Lifecycle summary. | |
no_secret_is_accepted_or_emitted_by_this_module |
Constant true. |
🔒 No output is sensitive, because this resource carries no credential at all.
A workspace is a feed, and a feed can be empty. This record creates no compute, publishes nothing and authorises nobody. Four things must exist before a person can launch a desktop — a host pool, an application group, a workspace and an association joining the last two — and then a role assignment scoped to the group. A clean apply here is one of five steps, and the module says so rather than letting a green plan imply otherwise.
The public-access default is inverted, and the divergence is stated twice. The provider defaults public_network_access_enabled to true; this module defaults it to false, per this suite's rule that a boolean gating exposure takes the closed value, and consistently with the already-authored host-pool module in this family. What makes the divergence worth arguing about is its cost: with public access off, the feed is reachable only through Azure Virtual Desktop Private Link, which needs a private endpoint on this workspace, another on the host pool, and DNS for the AVD private zones. None of that is created here. So the module emits both private_link_is_required_for_the_feed and a constant naming the divergence itself, and the ordinary internet-facing deployment is one typed argument away.
Omitting a display name is an instruction. The provider passes friendly_name and description on every create and update whether or not they are configured, so leaving one out sends an empty string. That is harmless on a greenfield apply and destructive on an adopted workspace: import a workspace the portal named, apply without these arguments, and the name is gone. friendly_name_was_supplied exists so a reviewer can tell which situation they are in.
Two spellings of one idea, in one family. This resource controls public access with a boolean; the host pool controls it with a four-valued string. They must be reasoned about together — a workspace open to clients in front of a host pool that is not leaves a half-open path that neither resource reports — and the asymmetry is exactly the kind that survives a spot-check, because both fields look right in isolation.
The name rule is mirrored, not tidied. 3 to 64 characters, beginning with a letter or digit and ending with a letter, digit or underscore. The trailing allowance for an underscore but not a dot or dash looks like an oversight and is the provider's actual rule; a neater version would reject legal input, and a validation {} failure blocks terraform destroy as well as apply.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Feed exposure | public_network_access_enabled = false — inverted from the provider's true, and matching the host-pool module in this family. |
Set it true for the ordinary internet-facing deployment. |
| The cost of that default | Reported, not hidden. private_link_is_required_for_the_feed names the three things this module cannot create. |
— |
| Display strings | Refused when empty, because the provider treats "" as an invalid length rather than an absent value. |
Omit the argument instead. |
| Names | Mirrored exactly, including the provider's underscore asymmetry. | — |
| Secrets | None accepted, none emitted. | — |
| Publishing and authorising | Neither is done here. The association and the role assignment are separate decisions with separate lifecycles. | — |
🔒 A
validation {}failure blocksterraform destroyas well as apply, which is why this module refuses only what the provider refuses and reports everything else.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the source at a tag — ?ref=v1.0.0 — never a branch. This module is authored and verified plan-only; a human applies from CI.
What validate and fmt cover, with no credentials:
- All 7 input validations, including the three-part name rule the published schema does not expose and the empty-string cases that a
compact()-based guard would silently skip. - The length limits on
friendly_nameanddescription. - The resource-group-name-not-an-ID check.
- HCL syntax and formatting.
What only plan or apply reaches:
- Whether
Microsoft.DesktopVirtualizationis registered in the subscription. - Whether the workspace name is already taken.
- Whether the Private Link path exists, when public access is disabled.
🔴 Nothing offline can tell you a user will see a desktop. That depends on an application group, an association and a role assignment, none of which this module creates.
Outputs:
avd_feed = {
"display_name" = "Production Desktops"
"id" = "/subscriptions/8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8/resourceGroups/rg-avd-eastus/providers/Microsoft.DesktopVirtualization/workspaces/ws-prod"
"named" = true
"needs_private" = false
"public" = true
}
description_was_supplied = true
location = "eastus"
| Symptom | Cause | Fix |
|---|---|---|
name must be between 3 and 64 characters… |
Shorter than three characters, or longer than 64. | Rename. It is force-new. |
name must BEGIN with a letter or digit and END with a letter, digit or UNDERSCORE |
A leading dash or underscore, or a trailing dot or dash. | Note the asymmetry: a trailing underscore is legal, a trailing dash is not. |
friendly_name must be between 1 and 64 characters when set |
It was set to an empty string. | Omit the argument. The provider treats "" as an invalid length, not an absent value. |
| Users see no desktops although the workspace exists | The workspace publishes nothing until an application group is associated with it, and authorises nobody. | Create the association resource, and a role assignment scoped to the application group. |
| Users cannot reach the feed at all | Public access is disabled — this module's default — and the Private Link path is incomplete. | Either set public_network_access_enabled = true, or add private endpoints on both the workspace and the host pool plus the AVD private DNS zones. |
| The display name disappeared after an apply | The provider sends friendly_name on every write, so omitting it clears the field. |
Set the argument. Check friendly_name_was_supplied. |
| An imported workspace reads back as public although the portal shows otherwise | The read treats any absent or non-Enabled value as public. |
Set the argument explicitly and apply. |
A change to location or resource_group_name plans a replacement |
Both are force-new, as is name. |
Expected — read force_new_fields. |
| Apply fails with an already-exists error | An import guard exists and the workspace was created in the portal. | terraform import it rather than applying over it. |
A timeouts key has no effect |
It was misspelled, and Terraform's object-type conversion discards undeclared keys silently. | Check the spelling against create / read / update / delete. |
azurerm_virtual_desktop_workspaceazurerm_virtual_desktop_workspace_application_group_association- Azure Virtual Desktop terminology
- Azure Virtual Desktop Private Link
- Sibling modules:
terraform-azurerm-virtual-desktop-application-group,terraform-azurerm-virtual-desktop-host-pool,terraform-azurerm-private-endpoint,terraform-azurerm-role-assignments,terraform-azurerm-resource-group - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."