Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Healthcare Workspace Terraform Module

Provisions an Azure Health Data Services workspace — the parent container and HIPAA / HITRUST compliance boundary for FHIR, DICOM, and MedTech service instances. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources


🧩 Overview

  • 🏥 Creates a single azurerm_healthcare_workspace — the logical container for all your Azure Health Data Services instances.
  • 🛡️ Establishes the HIPAA / HITRUST compliance boundary within which protected health information (PHI) travels.
  • 🧱 Keeps the workspace deliberately minimal: FHIR, DICOM, and MedTech services are separate sibling modules created inside this workspace.
  • 🔗 Emits the workspace id first so child service modules, private endpoints, diagnostics, and role assignments can wire to it.
  • 🏷️ Carries the universal tags and timeouts tail, and nothing else — there are no exposure knobs to misconfigure.

💡 Why it matters: In Azure Health Data Services the workspace must exist before any FHIR/DICOM/MedTech service can be created, and it is the boundary that regulators care about. Getting the workspace right — correct region, correct resource group, consistent tagging — is the foundation every downstream health-data service inherits.


❤️ Support this project

If this module saves you time, please consider supporting its continued development:


🗺️ Where this fits in the family

flowchart LR
  rg["terraform-azurerm-resource-group"]
  ws["terraform-azurerm-healthcare-workspace: the family keystone for Health Data Services"]
  fhir["terraform-azurerm-healthcare-fhir-service: the clinical data store"]
  dicom["terraform-azurerm-healthcare-dicom-service: imaging studies, DICOMweb"]
  medtech["terraform-azurerm-healthcare-medtech-service: device telemetry to FHIR, owns its FHIR destinations"]
  legacy["terraform-azurerm-healthcare-service: DEPRECATED Azure API for FHIR, retires 2026-09-30, NOT in a workspace"]
  ehns["terraform-azurerm-eventhub-namespace: device messages, consumed BY NAME"]
  kv["terraform-azurerm-key-vault: customer-managed keys"]
  sa["terraform-azurerm-storage-account: DICOM bring-your-own storage, and FHIR export"]
  acr["terraform-azurerm-container-registry: convert-data templates"]
  ra["terraform-azurerm-role-assignments: the grants each service identity needs, plus FHIR and DICOM data roles"]
  pe["terraform-azurerm-private-endpoint: private ingress. REQUIRED once public access is closed."]

  rg -->|"resource_group_name, location"| ws
  ws -->|"id, BY ID"| fhir
  ws -->|"id, BY ID"| dicom
  ws -->|"id, BY ID"| medtech
  ehns -->|"namespace, hub and consumer group BY NAME"| medtech
  fhir -->|"id as a destination_fhir_service_id"| medtech
  kv -->|"encryption_key_url"| dicom
  kv -->|"VERSIONLESS key id"| legacy
  sa -->|"bring-your-own storage BY ID"| dicom
  sa -->|"export account BY NAME"| fhir
  acr -->|"login server for convert-data"| fhir
  fhir -->|"identity_principal_id"| ra
  dicom -->|"identity_principal_id"| ra
  medtech -->|"identity_principal_id"| ra
  ra -->|"Event Hubs Data Receiver plus FHIR Data Writer"| medtech
  ra -->|"AcrPull plus Storage Blob Data Contributor"| fhir
  ra -->|"key and storage grants"| dicom
  pe -->|"reach the service privately"| fhir
  pe -->|"reach the service privately"| dicom
  legacy -->|"MIGRATE TO: workspace plus fhir-service"| ws

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef dep fill:#8a1c1c,stroke:#5c1010,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class ws keystone;
  class fhir,dicom,medtech me;
  class legacy dep;
  class rg,ehns,kv,sa,acr,ra,pe sib;
Loading

The workspace is the parent container. FHIR, DICOM, and MedTech services are separate modules that reference this workspace by id; the workspace itself owns none of them.


🧬 What this module builds

flowchart LR
  subgraph inputs["Inputs"]
    n["name"]
    rgn["resource_group_name"]
    loc["location"]
    tg["tags"]
    to["timeouts"]
  end
  ws["azurerm_healthcare_workspace.this"]
  subgraph outputs["Outputs"]
    oid["id"]
    onm["name"]
    org["resource_group_name"]
    oloc["location"]
    opec["private_endpoint_connections"]
  end
  n -->|"name"| ws
  rgn -->|"resource_group_name"| ws
  loc -->|"location"| ws
  tg -->|"tags"| ws
  to -->|"timeouts"| ws
  ws -->|"id"| oid
  ws -->|"name"| onm
  ws -->|"resource_group_name"| org
  ws -->|"location"| oloc
  ws -->|"private_endpoint_connection"| opec

  classDef keystone fill:#004578,stroke:#002b4d,color:#fff;
  classDef io fill:#eef2f7,stroke:#b8c4d0,color:#1b2733;
  class ws keystone;
  class n,rgn,loc,tg,to,oid,onm,org,oloc,opec io;
Loading

Resource inventory

Resource Count Role
azurerm_healthcare_workspace.this 1 The keystone workspace / compliance boundary.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
azurerm provider ~> 4.0
Provider block None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription.

Schema notes that bite (verified against the live provider schema):

  • name is force-new — renaming the workspace destroys and recreates it (and requires every child service to be gone first).
  • resource_group_name is force-new — moving the workspace to another resource group replaces it.
  • location is force-new — the region is fixed at creation; child services must live in the same region.
  • The workspace exposes no public_network_access_enabled, customer-managed-key, or identity {} arguments. Those data-plane controls belong to the child FHIR/DICOM/MedTech services, not the workspace.
  • 🔴 private_endpoint_connection is computed (read-only) and never populated at azurerm 4.81.0 -- the provider's flatten builds each entry and then discards it without appending, so the attribute is always an empty set however many private endpoints are approved. Nothing errors; an empty value simply reads as "none exist". Private endpoints are still created by a sibling module and still work; only this attribute is blind to them.

🔑 Required Azure RBAC Roles / Permissions

  • Contributor on the target resource group, or a custom role with Microsoft.HealthcareApis/workspaces/* plus the resource-group read actions, scoped to the resource group. This covers create, update, and delete of the workspace.
  • Data-plane roles for the child services (for example FHIR Data Contributor, DICOM Data Owner) are assigned separately against the child service scopes and are out of scope for this module.

Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.HealthcareApis resource provider registered on the target subscription.
  • The caller configures the provider "azurerm" { features {} } block, authentication, and subscription; this module declares none of them.

📁 Module Structure

terraform-azurerm-healthcare-workspace/
├── providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf   # name, resource_group_name, location + tags/timeouts tail
├── main.tf        # keystone azurerm_healthcare_workspace.this
├── outputs.tf     # id first, then name, resource_group_name, location, connections
├── README.md      # this document
├── SCOPE.md        # the cross-module contract
├── LICENSE        # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "healthcare_workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"

  tags = {
    environment = "prod"
    data_class  = "phi"
  }
}

ℹ️ The caller owns the provider: authentication, subscription, and the mandatory features {} block all live in the root module. Pin the module with ?ref=v1.0.0, never a branch.


🔌 Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group (name)
location string caller / terraform-azurerm-resource-group (location)

Emits

Output Description Consumed by
id Workspace Resource ID (first) child FHIR / DICOM / MedTech modules, private endpoints, diagnostics, role assignments
name Workspace name diagnostics / tagging
resource_group_name Resource group holding the workspace compositions wiring child services
location Workspace region child service placement (must match)
private_endpoint_connections 🔴 Always an empty set at azurerm 4.81.0 — the provider's flatten discards every entry. do not use for topology documentation

📚 Example Library

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "logs_id" {
  description = "id of an existing logs that these examples reference but do not create."
  type        = string
}

variable "network_subnet_ids" {
  description = "subnet ids of an existing network that these examples reference but do not create."
  type        = map(string)
}
1 · Minimal workspace (the smallest real call)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

💡 The empty call already produces the compliance boundary. There is nothing to lock down on the workspace itself — data-plane security lives on the child services.

2 · Workspace with governance tags
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"

  tags = {
    environment = "prod"
    data_class  = "phi"
    cost_center = "clinical-platform"
    compliance  = "hipaa-hitrust"
  }
}

🔒 Tag PHI-bearing resources consistently — downstream policy and cost reporting rely on it.

3 · Custom operation timeouts
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}

ℹ️ Deleting a workspace can be slow when child services were only recently removed; a longer delete timeout avoids spurious failures.

4 · A second workspace in a paired region
module "workspace_primary" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophieast"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

module "workspace_secondary" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophiwest"
  resource_group_name = "rg-health-westus2"
  location            = "westus2"
}

⚠️ Region is immutable. Model multi-region topologies as separate workspace instances, never by changing location on an existing one.

5 · One workspace per environment with for_each
locals {
  workspaces = {
    dev  = { name = "contosophidev", rg = "rg-health-dev", location = "eastus2" }
    test = { name = "contosophitest", rg = "rg-health-test", location = "eastus2" }
    prod = { name = "contosophiprod", rg = "rg-health-prod", location = "eastus" }
  }
}

module "workspace" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"
  for_each = local.workspaces

  name                = each.value.name
  resource_group_name = each.value.rg
  location            = each.value.location

  tags = {
    environment = each.key
    data_class  = "phi"
  }
}

💡 Keying by a stable environment name means adding or removing one environment never re-indexes the others.

6 · Naming convention at scale
variable "workspaces" {
  type = map(object({
    location = string
  }))
  default = {
    clinical = { location = "eastus" }
    research = { location = "eastus2" }
    imaging  = { location = "centralus" }
  }
}

module "workspace" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"
  for_each = var.workspaces

  name                = "phi${each.key}01"
  resource_group_name = "rg-health-${each.key}"
  location            = each.value.location
}

ℹ️ Workspace names are 1-24 characters, lowercase letters and digits only (the provider's validator is ^[a-z0-9]{1,24}$). Uppercase is rejected, and one- and two-character names are legal. No hyphens or underscores. Compose names accordingly.

7 · Attaching a FHIR service (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# FHIR service is a SEPARATE module — it lives inside this workspace by id.
# module "fhir" {
#   source       = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-fhir-service.git?ref=v1.0.0"
#   workspace_id = module.workspace.id
#   location     = module.workspace.location
#   ...
# }

ℹ️ The FHIR service is created inside the workspace and consumes module.workspace.id. Deploy the workspace first.

8 · Attaching a DICOM service (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# DICOM service is a SEPARATE module.
# module "dicom" {
#   source       = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-dicom-service.git?ref=v1.0.0"
#   workspace_id = module.workspace.id
#   location     = module.workspace.location
#   ...
# }

ℹ️ DICOM medical-imaging endpoints are also workspace children; they must be created in the workspace's region.

9 · Attaching a MedTech service (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# MedTech (IoT device data -> FHIR) is a SEPARATE module.
# module "medtech" {
#   source       = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-medtech-service.git?ref=v1.0.0"
#   workspace_id = module.workspace.id
#   ...
# }

💡 MedTech ingests device telemetry and maps it into a FHIR service within the same workspace.

10 · Private access via a private endpoint (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# Private connectivity is owned by the private-endpoint module and targets the workspace id.
# module "workspace_pe" {
#   source                         = "git::https://github.com/microsoftexpert/terraform-azurerm-private-endpoint.git?ref=v1.0.0"
#   private_connection_resource_id = module.workspace.id
#   subresource_names              = ["healthcareworkspace"]
#   subnet_id                      = var.network_subnet_ids["health"]
#   ...
# }

🔴 Established connections do not appear on the workspace's private_endpoint_connections output: it is never populated at azurerm 4.81.0 -- the provider's flatten builds each entry and then discards it without appending, so the attribute is always an empty set however many private endpoints are approved. Nothing errors; an empty value simply reads as "none exist". Read the private endpoints themselves instead.

11 · Diagnostic settings (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# Route platform logs/metrics with the diagnostic-setting module, targeting the workspace id.
# module "workspace_diag" {
#   source                     = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"
#   target_resource_id         = module.workspace.id
#   log_analytics_workspace_id = var.logs_id
#   ...
# }

ℹ️ Application monitoring is a workspace-level configuration; a dedicated module keeps the diagnostics contract reusable.

12 · Entra RBAC data-plane assignment (sibling module)
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = "rg-health-eastus"
  location            = "eastus"
}

# Grant least-privilege data-plane roles with the role-assignments module, scoped to the workspace id.
# module "workspace_rbac" {
#   source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
#   scope  = module.workspace.id
#   assignments = {
#     fhir_reader = { role_definition_name = "FHIR Data Reader", principal_id = var.app_object_id }
#   }
# }

🔒 Access to PHI is Entra-based; there are no keys. Assign the smallest role that works at the smallest scope.

13 · Consuming a resource-group module's outputs
module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-health-eastus"
  location = "eastus"
}

module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophi01"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

💡 Wiring the resource group's name and location in keeps the workspace inside the group's lifecycle and region.

14 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

# 1) Resource group
module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-health-prod-eastus"
  location = "eastus"

  tags = {
    environment = "prod"
    data_class  = "phi"
  }
}

# 2) Health Data Services workspace (this module) — the compliance boundary
module "workspace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-workspace.git?ref=v1.0.0"

  name                = "contosophiprod"
  resource_group_name = module.rg.name
  location            = module.rg.location

  tags = {
    environment = "prod"
    data_class  = "phi"
    compliance  = "hipaa-hitrust"
  }
}

# 3) FHIR service (SEPARATE sibling module) created INSIDE the workspace by id
# module "fhir" {
#   source       = "git::https://github.com/microsoftexpert/terraform-azurerm-healthcare-fhir-service.git?ref=v1.0.0"
#   name         = "contosofhir"
#   workspace_id = module.workspace.id
#   location     = module.workspace.location
#
#   tags = {
#     environment = "prod"
#     data_class  = "phi"
#   }
# }

🔒 Order matters: the resource group, then the workspace, then each child service. The FHIR service references module.workspace.id and must be created in module.workspace.location.


📥 Inputs

Required

Name Type Description
name string Workspace name — 1-24 characters, lowercase letters and digits only (the provider's validator is ^[a-z0-9]{1,24}$). Uppercase is rejected, and one- and two-character names are legal. Unique in the subscription. Force-new.
resource_group_name string Existing resource group to hold the workspace. Force-new.
location string Azure region. Immutable after creation. Force-new.

Optional (universal tail)

Name Type Default Description
tags map(string) {} Tags applied to the workspace.
timeouts object(...) null Create / read / update / delete timeouts.
Full object() schemas
variable "name" {
  type = string
    # 1-24 characters, lowercase letters and digits only; force-new.
}

variable "resource_group_name" {
  type = string
  # Existing resource group; force-new.
}

variable "location" {
  type = string
  # Azure region; immutable / force-new.
}

variable "tags" {
  type    = map(string)
  default = {}
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Kind
id The Azure Resource ID of the Health Data Services workspace (/subscriptions/.../resourceGroups//providers/Microsoft.HealthcareApis/workspaces/) Passthrough
name The name of the workspace Passthrough
resource_group_name The resource group holding the workspace Passthrough
location The Azure region of the workspace, in the provider's normalized form (for example "eastus" even when "East US" was supplied) Passthrough
required_child_service_location The region every FHIR, DICOM and MedTech service inside this workspace must be created in Passthrough
tags The tags applied to the workspace Passthrough
private_endpoint_connections The workspace's private endpoint connections, each with an id and a name, as the provider reports them Passthrough
private_endpoint_connections_are_never_reported Constant true, and it earns its place because the failure is completely silent Constant
private_link_is_the_only_workspace_level_network_control Constant true Constant
is_compliance_boundary_only Constant true Constant
workspace_has_no_managed_identity Constant true, stated because compositions routinely assume otherwise Constant
fhir_public_access_is_not_configurable_from_terraform Constant true, and consequential for anyone closing public access as a control Constant
customer_managed_keys_supported_on_dicom_only Constant true Constant
deletion_is_blocked_while_child_services_exist Constant true Constant
resource_group_deletion_bypasses_the_child_guard Constant true, and the single most dangerous fact about this resource Constant
data_protection_review_belongs_outside_this_module Constant true, recorded as a referral rather than an answer Constant
tags_are_the_only_updatable_field Constant true Constant
plan_requires_no_credential_read Constant true Constant

🧠 Architecture Notes

  • The workspace is a boundary, not a data plane. It has no public-access, encryption, or identity arguments of its own. Every PHI security control — customer-managed keys, private endpoints, Entra RBAC, diagnostics — is configured on the child FHIR/DICOM/MedTech services or by sibling modules. This module keeps the boundary minimal on purpose.
  • Force-new triple. name, resource_group_name, and location each force replacement. Because a workspace cannot be deleted while child services exist, an accidental change to any of these can block a plan until the children are removed. Treat these three as fixed after first apply.
  • Region is permanent. The region is chosen at creation and cannot change; child services must be created in the workspace's region. Multi-region designs use multiple workspace instances.
  • Deletion ordering. Deleting the workspace requires all child services to be gone first. In a composition, that means the child modules are destroyed before this one — the implicit dependency created by passing module.workspace.id into the children enforces the correct order.
  • timeouts is rendered with a dynamic block and try(...) on each field, so an omitted timeouts object renders as absent rather than as an error.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block. If the module appears not to initialize in isolation, that missing block is almost always the cause — it belongs to the root module, never here.

🧱 Design Principles

The workspace exposes no exposure toggles, so the secure-defaults table is short — the security surface lives on the child services this module deliberately does not own:

Concern Secure posture (empty call) Opt-out Where it lives
Public network access Not configurable on the workspace — Child FHIR / DICOM / MedTech services
Encryption at rest On by default (platform-managed); CMK optional supply a key Child services
Data-plane auth Entra RBAC only — no keys — Child services + role-assignments sibling
Private connectivity Off until a private endpoint is attached attach a private endpoint private-endpoint sibling
Diagnostics Off until wired wire a diagnostic setting monitor-diagnostic-setting sibling

The workspace's own contribution to security is that it is the compliance boundary; nothing about the empty call opens exposure.


🚀 Runbook

# From the module folder — plan-only, no cloud calls:
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • No terraform apply from this workflow. A human applies from CI against real credentials.

🧪 Testing

The offline proof gate is what this module guarantees:

Check What it proves Calls Azure?
terraform init -backend=false Provider resolves at ~> 4.0; no backend needed. No
terraform validate Configuration is internally consistent and type-correct against the pinned schema. No
terraform fmt -check Canonical formatting. No

Only terraform plan (run by a human from CI) exercises the ARM API. This library never runs plan or apply during authoring.


💬 Example Output

$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-health-eastus/providers/Microsoft.HealthcareApis/workspaces/contosophi01"
name = "contosophi01"
resource_group_name = "rg-health-eastus"
location = "eastus"
private_endpoint_connections = toset([])

🔍 Troubleshooting

Symptom Cause Fix
provider ... features error on init The root module is missing provider "azurerm" { features {} }. Add the features {} block to the caller's provider — it never lives in this module.
Plan wants to replace the workspace name, resource_group_name, or location changed — all are force-new. Revert the change, or accept replacement only after every child service is removed.
Cannot delete the workspace Child FHIR / DICOM / MedTech services still exist. Destroy the child service modules first, then the workspace.
name rejected at validate The name is not 1-24 lowercase letters and digits. The provider's validator catches the shape before any API call. Use lowercase letters and digits only, 1-24 chars.
name rejected at apply The name collides with an existing workspace in the subscription. Only the collision reaches Azure. Choose another name.
Child service create fails on region The child was placed in a different region than the workspace. Set the child's location to module.workspace.location.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."