Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Dev Test Lab Terraform Module

Provisions an Azure Dev Test Lab β€” the governance and cost-sandbox container that dev/test compute runs inside β€” as a focused standalone primitive, targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Posture


🧩 Overview

  • πŸ§ͺ Creates one azurerm_dev_test_lab β€” the container that scopes and governs a set of dev/test virtual machines.
  • πŸ” Auto-provisions a Key Vault (emitted as key_vault_id) so the VMs that run in the lab can source secrets by reference rather than embedding them.
  • πŸ’Ύ Auto-provisions the backing storage accounts (artifacts, standard, premium, premium-data-disk) the lab's VMs consume β€” all surfaced as computed outputs.
  • 🏷️ Carries tags for cost-center / owner / environment metadata so lab spend can be grouped and reported.
  • 🧱 Stays deliberately small: compute, cost schedules, policies, and lab networks are separate sibling modules consumed by id.

πŸ’‘ Why it matters: A Dev Test Lab is a cost-and-governance boundary, not a workload. Keeping the lab a thin primitive lets one lab anchor many downstream modules β€” the auto-shutdown schedules and lab policies that actually control spend β€” without this module owning their lifecycles.

❀️ Support this project

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

πŸ—ΊοΈ Where this fits in the family

graph LR
  rg["terraform-azurerm-resource-group"]
  mod["terraform-azurerm-dev-test-lab"]
  labres["azurerm_dev_test_lab"]
  vm["terraform-azurerm-dev-test-linux-virtual-machine and its Windows twin"]
  sched["terraform-azurerm-dev-test-schedule"]
  pol["terraform-azurerm-dev-test-policy"]
  rg -->|"resource_group_name + location"| mod
  mod -->|"creates keystone"| labres
  labres -->|"lab id + key_vault_id"| vm
  labres -->|"cost-control scope"| sched
  labres -->|"governance scope"| pol
  classDef this fill:#0078D4,color:#fff,stroke:#004578,stroke-width:1px;
  classDef keystone fill:#004578,color:#fff,stroke:#004578,stroke-width:1px;
  classDef sib fill:#f2f2f2,color:#333333,stroke:#bbbbbb,stroke-width:1px;
  class mod this;
  class labres keystone;
  class rg,vm,sched,pol sib;
Loading

🧬 What this module builds

graph LR
  identity["name + location + resource_group_name"]
  meta["tags + timeouts"]
  mod["terraform-azurerm-dev-test-lab"]
  lab["azurerm_dev_test_lab.this"]
  outCore["id + name + unique_identifier"]
  outVault["key_vault_id (auto-provisioned)"]
  outStorage["storage account ids (auto-provisioned)"]
  identity -->|"required inputs"| mod
  meta -->|"optional inputs"| mod
  mod -->|"manages"| lab
  lab -->|"emits first"| outCore
  lab -->|"emits"| outVault
  lab -->|"emits"| outStorage
  classDef this fill:#0078D4,color:#fff,stroke:#004578,stroke-width:1px;
  classDef keystone fill:#004578,color:#fff,stroke:#004578,stroke-width:1px;
  classDef io fill:#f2f2f2,color:#333333,stroke:#bbbbbb,stroke-width:1px;
  class mod this;
  class lab keystone;
  class identity,meta,outCore,outVault,outStorage io;
Loading

Resource inventory

Resource Role Cardinality
azurerm_dev_test_lab.this Keystone β€” the lab governance container 1

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider hashicorp/azurerm ~> 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 immutable β€” it is embedded in the lab's Azure Resource ID, so changing it forces replacement.
  • resource_group_name is immutable β€” changing it forces replacement.
  • location is force-new β€” moving the lab between regions replaces it.
  • There is no storage_type argument on the lab in the ~> 4.0 line. Storage tiering (Standard vs Premium) is a property of the dev/test VM resources that run inside the lab, not of the lab. Setting storage_type here is an unsupported argument.
  • The Key Vault and storage accounts the lab creates are computed outputs, not inputs β€” do not try to configure them through this module.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • DevTest Labs User to consume an existing lab.
  • Contributor (or equivalent administrative access) on the target resource group to create and manage the lab. Because creation also provisions a Key Vault and storage accounts on the caller's behalf, the identity must be able to create those resource types in the target resource group.
  • Grant at the smallest scope that works β€” prefer the resource group over the subscription.

Azure Prerequisites

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

πŸ“ Module Structure

terraform-azurerm-dev-test-lab/
β”œβ”€β”€ providers.tf   # required_version + azurerm ~> 4.0 pin; no provider block
β”œβ”€β”€ variables.tf   # deeply-typed inputs; tags + timeouts universal tail
β”œβ”€β”€ main.tf        # keystone azurerm_dev_test_lab.this + dynamic timeouts
β”œβ”€β”€ outputs.tf     # id first, then name, then computed lab references
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # cross-module contract
β”œβ”€β”€ LICENSE        # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore     # canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
  # auth + subscription are the caller's concern (Azure CLI, Managed Identity, OIDC, ...)
}

module "dev_test_lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-sandbox-eastus2"
  location            = "eastus2"
  resource_group_name = "rg-devtest-eastus2"

  tags = {
    environment = "sandbox"
    cost_center = "engineering"
  }
}

ℹ️ Pin ?ref=v1.0.0 β€” never a branch. The caller owns the provider, auth, and the mandatory features {} block; the module never declares them.

πŸ”Œ 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 Dev Test Lab Resource ID (first) VM / schedule / policy / network siblings
name Dev Test Lab name diagnostics / tagging
unique_identifier Immutable Azure identifier audit / correlation
key_vault_id Auto-provisioned Key Vault ID out-of-band secret provisioning
artifacts_storage_account_id Artifacts storage account ID lab tooling
default_storage_account_id Default (standard) storage account ID lab VMs
default_premium_storage_account_id Default premium storage account ID lab VMs
premium_data_disk_storage_account_id Premium data-disk storage account ID lab VMs

πŸ“š Example Library

1 Β· Minimal lab (the secure empty call)
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-min"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

πŸ”’ The lab surface exposes no public-access or protection toggles, so the minimal call is already the hardened resource. Cost and exposure decisions live on the sibling modules that run workloads inside it.

2 Β· Cost-attribution tags
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-tagged"
  location            = "eastus2"
  resource_group_name = "rg-devtest"

  tags = {
    environment = "sandbox"
    cost_center = "platform-eng"
    owner       = "team-a"
    expiry      = "2026-12-31"
  }
}

πŸ’‘ Tags are the primary lever for grouping and reporting lab spend. Standardize cost_center and owner across every lab so finance can attribute the sandbox bill.

3 Β· Custom timeouts
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-timeouts"
  location            = "westus2"
  resource_group_name = "rg-devtest"

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

ℹ️ Lab creation fans out to a Key Vault and several storage accounts, so a longer create/delete window is reasonable in busy subscriptions.

4 Β· A specific US region
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-central"
  location            = "centralus"
  resource_group_name = "rg-devtest-central"
}

⚠️ location is force-new. Choose the region deliberately β€” moving it later replaces the lab and everything keyed off its Resource ID.

5 Β· One lab per team with for_each
variable "teams" {
  type = map(object({
    location = string
    rg_name  = string
  }))
  default = {
    payments = { location = "eastus", rg_name = "rg-devtest-payments" }
    lending  = { location = "eastus2", rg_name = "rg-devtest-lending" }
    data     = { location = "westus2", rg_name = "rg-devtest-data" }
  }
}

module "lab" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"
  for_each = var.teams

  name                = "lab-${each.key}"
  location            = each.value.location
  resource_group_name = each.value.rg_name

  tags = { team = each.key }
}

πŸ’‘ for_each over a keyed map keeps each team's lab stable β€” adding or removing a team never re-indexes the rest.

6 Β· Consuming the auto-provisioned Key Vault
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-secrets"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

output "lab_key_vault_id" {
  value = module.lab.key_vault_id
}

πŸ”’ The lab creates its own Key Vault for VM secrets. This module emits only the vault's id β€” provision secret values out of band (a Key Vault reference, a pipeline secret), never in Terraform state.

7 Β· Dev/test VM sibling β€” Standard storage
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-std"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

# Storage tiering lives on the VM, not the lab. The VM sibling module sets
# storage_type and references this lab BY NAME -- it is not owned here. The lab
# network module (declared in full in the composition below) supplies the subnet.
module "lab_vm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-linux-virtual-machine.git?ref=v1.0.0"

  name                = "vm-dev-01"
  lab_name            = module.lab.name
  resource_group_name = "rg-devtest"
  location            = "eastus"

  size         = "Standard_DS2_v2"
  username     = "devuser"
  storage_type = "Standard"
  ssh_key      = file("~/.ssh/id_rsa.pub")

  lab_virtual_network_id = module.lab_net.id
  lab_subnet_name        = module.lab_net.expected_subnet_name_derived_from_the_network_name

  gallery_image_reference = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }
}

ℹ️ In azurerm ~> 4.0 the lab has no storage_type. "A Standard-storage lab" means its VMs request storage_type = "Standard", set on the VM sibling module.

8 Β· Dev/test VM sibling β€” Premium storage
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-premium"
  location            = "eastus2"
  resource_group_name = "rg-devtest"
}

# The lab already surfaces default_premium_storage_account_id for premium disks.
# The VM sibling is not owned here; this shows how it attaches. The lab network
# module (declared in full in the composition below) supplies the subnet.
module "lab_vm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-linux-virtual-machine.git?ref=v1.0.0"

  name                = "vm-dev-premium"
  lab_name            = module.lab.name
  resource_group_name = "rg-devtest"
  location            = "eastus"

  size         = "Standard_DS2_v2"
  username     = "devuser"
  storage_type = "Premium"
  ssh_key      = file("~/.ssh/id_rsa.pub")

  lab_virtual_network_id = module.lab_net.id
  lab_subnet_name        = module.lab_net.expected_subnet_name_derived_from_the_network_name

  gallery_image_reference = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }
}

output "premium_storage" {
  value = module.lab.default_premium_storage_account_id
}

πŸ’‘ The lab provisions both standard and premium backing storage. A "Premium lab" is one whose VMs opt into storage_type = "Premium"; the premium account ID is ready to consume via default_premium_storage_account_id.

9 Β· Auto-shutdown schedule sibling (the cost control)
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-scheduled"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

# Auto-shutdown is the single most effective lab cost control. It is a separate
# module, and it references this lab BY NAME rather than by id.
module "lab_autoshutdown" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-schedule.git?ref=v1.0.0"

  name                = "LabVmsShutdown"
  resource_group_name = "rg-devtest"
  location            = "eastus"
  lab_name            = module.lab.name # the NAME -- this is what creates the dependency edge

  task_type    = "LabVmsShutdown"
  time_zone_id = "Eastern Standard Time"
  status       = "Enabled" # the provider defaults to Disabled

  daily_recurrence      = { time = "1900" }
  notification_settings = {}
}

πŸ’‘ Keeping the schedule external means one lab can carry a shutdown policy that finance owns, without this module taking on the schedule's lifecycle.

⚠️ Note lab_name = module.lab.name, not an id: the schedule resource takes the lab's name plus a resource_group_name, so a literal would leave Terraform with no dependency edge to this lab at all. And status must be typed β€” the provider creates a schedule Disabled by default.

10 Β· Lab policy sibling (the guardrails)
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-guarded"
  location            = "eastus2"
  resource_group_name = "rg-devtest"
}

# VM size / count / allowed-image guardrails are lab policies, owned by a sibling module.
# Note the policy's `name` SELECTS which guardrail it is, from a closed set of eight.
module "max_vms_per_user" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-policy.git?ref=v1.0.0"

  name                = "UserOwnedLabVmCount"
  policy_set_name     = "default"
  lab_name            = module.lab.name
  resource_group_name = "rg-devtest"

  evaluator_type = "MaxValuePolicy"
  threshold      = "3"
  description    = "At most three machines per user."
}

πŸ”’ Guardrails (max VMs per user, allowed VM sizes, allowed images) are the governance half of the sandbox. They belong to the policy sibling so they can be reviewed and versioned independently.

11 Β· Consuming the backing storage account outputs
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-storage"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

output "artifacts_storage"      { value = module.lab.artifacts_storage_account_id }
output "default_storage"        { value = module.lab.default_storage_account_id }
output "premium_data_disk_sa"   { value = module.lab.premium_data_disk_storage_account_id }

ℹ️ These accounts are created and managed by the lab. Read their IDs when a sibling needs to reference them; do not attempt to reconfigure them through this module.

12 Β· Least-privilege consumption (existing lab)
# A team member with only the DevTest Labs User role can reference an existing
# lab by id/name without holding rights to create it.
data "azurerm_dev_test_lab" "existing" {
  name                = "lab-shared"
  resource_group_name = "rg-devtest"
}

# Pass data.azurerm_dev_test_lab.existing.id into sibling VM/schedule modules.

πŸ”’ Separate the identity that creates the lab (Contributor on the RG) from the identities that use it (DevTest Labs User). Grant creation rights only in the pipeline that owns this module.

13 Β· Diagnostics via a sibling monitor module
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-observed"
  location            = "eastus"
  resource_group_name = "rg-devtest"
}

# Diagnostics are consumed by id from a reusable sibling module. `name` and
# `target_resource_id` are both REQUIRED there -- the earlier form omitted `name`.
module "lab_diagnostics" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"

  name                       = "diag-lab-platform"
  target_resource_id         = module.lab.id
  log_analytics_workspace_id = var.log_analytics_workspace_id # pre-existing; not created here
}

πŸ’‘ The lab emits its id first precisely so cross-cutting modules (diagnostics, RBAC, private endpoints) can attach to it without this module owning them.

14 Β· Wiring from a resource-group sibling
module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-devtest-eastus2"
  location = "eastus2"
}

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

  name                = "lab-from-rg"
  location            = module.rg.location
  resource_group_name = module.rg.name

  tags = { environment = "sandbox" }
}

ℹ️ The lab references the resource group; it never creates one. Feed module.rg.name and module.rg.location straight in.

15 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

# 1) The resource group that contains the sandbox.
module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-devtest-eastus2"
  location = "eastus2"

  tags = { environment = "sandbox", cost_center = "engineering" }
}

# 2) The lab (this module) β€” the governance/cost container.
module "lab" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-lab.git?ref=v1.0.0"

  name                = "lab-sandbox-eastus2"
  location            = module.rg.location
  resource_group_name = module.rg.name

  tags = { environment = "sandbox", cost_center = "engineering" }
}

# 3) The lab's own network wrapper. Its subnet's name is DERIVED from this name, so the
#    module predicts it at plan time -- the resource's own subnet_name is computed.
module "lab_net" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-virtual-network.git?ref=v1.0.0"

  name                = "lab-net"
  lab_name            = module.lab.name
  resource_group_name = module.rg.name

  subnet = {
    use_in_virtual_machine_creation = "Allow"
    use_public_ip_address           = "Deny"
  }
}

# 4) A dev/test VM sibling that runs INSIDE the lab (a separate module).
#    It references the lab BY NAME. Note it takes no key_vault_id: the lab's
#    auto-provisioned vault is consumed out of band, not passed to the VM.
module "lab_vm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-dev-test-linux-virtual-machine.git?ref=v1.0.0"

  name                = "vm-dev-01"
  lab_name            = module.lab.name
  resource_group_name = module.rg.name
  location            = module.rg.location

  size         = "Standard_DS2_v2"
  username     = "devuser"
  storage_type = "Standard"
  ssh_key      = file("~/.ssh/id_rsa.pub")

  lab_virtual_network_id = module.lab_net.id
  lab_subnet_name        = module.lab_net.expected_subnet_name_derived_from_the_network_name

  gallery_image_reference = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }
}

output "lab_id"           { value = module.lab.id }
output "lab_key_vault_id" { value = module.lab.key_vault_id }

πŸ”’ The lab is the anchor: the resource group contains it, the VM sibling runs inside it and draws secrets from its auto-provisioned vault, and the cost schedule / policy siblings (examples 9 and 10) attach to module.lab.id. This module owns only the lab.

πŸ“₯ Inputs

Identity

Name Type Required Default
name string βœ… β€”

Placement

Name Type Required Default
location string βœ… β€”
resource_group_name string βœ… β€”

Metadata (universal tail)

Name Type Required Default
tags map(string) ❌ {}
timeouts object({...}) ❌ null
Full input schemas
variable "name" {
  type = string
  # Immutable β€” changing forces replacement (embedded in the Resource ID).
}

variable "location" {
  type = string
  # US Azure regions (eastus, eastus2, westus2, centralus). Force-new.
}

variable "resource_group_name" {
  type = string
  # Existing resource group; referenced, not created. Immutable.
}

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 Dev Test Lab, of the form /subscriptions//resourceGroups//providers/Microsoft.DevTestLab/labs/ Passthrough
name The name of the Dev Test Lab Passthrough
location Azure region, in the canonical form Azure uses Passthrough
unique_identifier The unique immutable identifier assigned to the lab by Azure - a GUID, per the ARM property definition, distinct from the Resource ID and stable for the life of the lab Passthrough
key_vault_id Resource ID of the Key Vault the lab auto-provisions for VM secrets, named followed by four digits Passthrough
artifacts_storage_account_id Resource ID of the storage account the lab uses for artifacts Passthrough
default_storage_account_id Resource ID of the lab's default (standard) storage account Passthrough
default_premium_storage_account_id Resource ID of the lab's default premium storage account Passthrough
premium_data_disk_storage_account_id Resource ID of the storage account used for the lab's premium data disks Passthrough
lab_id_suffix The subscription-independent tail of the lab's Resource ID, assembled from resource_group_name and name: resourceGroups//providers/Microsoft.DevTestLab/labs/ Derived
resource_group_name The resource group containing the lab, as configured Derived
location_normalized The lab's region in the form the provider actually sends and compares: lowercased with spaces removed Passthrough
subscription_id The subscription containing the lab, parsed from the lab's Resource ID Derived
key_vault_id_is_empty_string_when_absent Always true, and it changes how you must test the key_vault_id output Constant
has_key_vault Whether the service reported a Key Vault for this lab Passthrough
destroy_deletes_all_lab_vms_and_their_resource_groups Constant
destroy_does_not_delete_the_labs_resource_group Always true Constant
labs_resource_group_cannot_be_deleted_while_the_lab_exists Always true, and it dictates destroy ordering Constant
creates_a_resource_group_per_lab_vm Always true, and it is a subscription-quota fact rather than a lab fact Constant
implicitly_provisioned_resources Derived
service_itself_is_free_and_the_bill_is_the_resources_it_creates Always true Constant
storage_tier_the_service_defaults_to Derived
lab_is_not_network_isolated Constant
unmanaged_lab_properties Derived
sends_no_lab_properties_on_create_or_update Always true Constant
force_new_arguments The arguments that force replacement of the lab when changed: name, location and resource_group_name Derived
in_place_updatable_arguments The only arguments this module can change without replacing the lab: tags Derived
resource_group_name_is_compared_case_insensitively Always true Constant
location_is_compared_after_normalization Always true Constant
create_performs_an_existence_check_first Always true unless the caller has enabled the provider feature that skips import checks Constant
read_fails_if_the_services_vault_reference_is_unparseable Always true Constant
arm_api_version The ARM API version the provider uses for this resource: 2018-09-15 Derived
timeout_defaults The provider's built-in timeouts for this resource, which appear nowhere in the schema Derived
effective_timeouts The timeouts actually in force: the caller's values where supplied, the provider's defaults otherwise Derived

🧠 Architecture Notes

  • The lab is a container, not a workload. main.tf renders exactly one resource. Every knob that controls cost or exposure (auto-shutdown, VM count/size policies, allowed images, network access) is deliberately out of scope and owned by sibling modules that consume this lab's id.
  • Immutable identity. name, resource_group_name, and location are all force-new β€” they are baked into the lab's Azure Resource ID or otherwise fixed at creation. Treat a rename or region move as a destroy-and-recreate, and expect anything keyed off the lab's id to cascade.
  • Auto-provisioned dependencies are outputs, not inputs. On creation, the lab stands up a Key Vault and several storage accounts. This module never accepts configuration for them; it surfaces their IDs so downstream modules can reference them. The Key Vault ID is a reference only β€” secret values are provisioned out of band, and this module emits no secret.
  • No storage_type on the lab. Storage tiering belongs to the dev/test VM resources, verified against the live provider schema. Passing storage_type to this module is an unsupported argument.
  • timeouts is rendered as a dynamic block guarded by try(...), so an omitted timeout falls back to the provider default rather than erroring.
  • features {} dependence. The provider will not initialize without the caller's provider "azurerm" { features {} } block. That block is the root module's responsibility; this module never carries one.

🧱 Design Principles

The lab surface exposes no exposure or protection toggles of its own, so its secure posture is expressed by what is deliberately delegated. The risky relaxations live on the sibling modules β€” the caller must reach for them explicitly.

Concern Secure default (empty call) Opt-out (caller must type it)
VM auto-shutdown / cost control Off the lab; provisioned by the schedule sibling Add the schedule sibling and set an always-on window
VM count / size / image guardrails Off the lab; provisioned by the policy sibling Add the policy sibling and loosen thresholds
Secret handling Secrets referenced from the auto-provisioned Key Vault Embed secrets elsewhere (discouraged)
Diagnostics / RBAC / private endpoints Composed from siblings by id Attach the relevant sibling module

πŸš€ Runbook

# From the module folder (plan-only; a human applies from CI):
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 in source β€” never a branch.
  • No cloud apply happens here. init -backend=false, validate, and fmt -check are static, offline checks; only a human runs terraform plan / apply against real credentials from CI.

πŸ§ͺ Testing

Check What it proves Reaches Azure?
terraform init -backend=false Provider pin resolves; module initializes without a backend No
terraform validate Configuration is internally consistent and type-correct against the pinned provider schema No
terraform fmt -check Canonical formatting No
terraform plan (human, from CI) ARM will accept the request; force-new / quota behavior Yes β€” not part of authoring

πŸ’¬ Example Output

Outputs:

id                                   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-devtest-eastus2/providers/Microsoft.DevTestLab/labs/lab-sandbox-eastus2"
name                                 = "lab-sandbox-eastus2"
unique_identifier                    = "4b8f2c1a-9d3e-4f0a-bc21-7e5a1d9c6f04"
key_vault_id                         = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-devtest-eastus2/providers/Microsoft.KeyVault/vaults/labsandbox1234"
artifacts_storage_account_id         = "/subscriptions/.../providers/Microsoft.Storage/storageAccounts/alabsandbox5678"
default_storage_account_id           = "/subscriptions/.../providers/Microsoft.Storage/storageAccounts/dlabsandbox9012"
default_premium_storage_account_id   = "/subscriptions/.../providers/Microsoft.Storage/storageAccounts/plabsandbox3456"
premium_data_disk_storage_account_id = "/subscriptions/.../providers/Microsoft.Storage/storageAccounts/pdlabsandb0789"

πŸ” Troubleshooting

Symptom Cause Fix
Unsupported argument: storage_type Storage tiering was set on the lab; it does not exist there in ~> 4.0 Remove it from the lab call and set storage_type on the dev/test VM sibling instead
Plan shows the lab being destroyed and recreated A force-new field (name, resource_group_name, or location) changed Revert the change, or accept replacement and rewire everything keyed off the lab id
Error: features block is required The caller's provider has no features {} block Add provider "azurerm" { features {} } to the root module
AuthorizationFailed creating the lab Identity lacks rights to create the lab or its backing Key Vault / storage Grant Contributor on the target resource group to the creating identity
Backing storage / Key Vault not configurable Those resources are computed outputs of the lab, not inputs Read them via the *_storage_account_id / key_vault_id outputs; manage them in their own modules if needed

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."