Automating the Creation of a Microsoft Azure DevTest Lab

geoinformatica

Automating the Creation of a Microsoft Azure DevTest Lab

Creating an automated script to deploy an Azure DevTest Lab with virtual machines and Bastion enabled requires a structured approach. If you haven’t already, review existing Microsoft Azure help docs: Quickstart: Create a lab in Azure DevTest Labs using Terraform – Azure DevTest Labs | Microsoft Learn

TecnetOne has a simple network diagram that describes this setup

Below are the best steps to start:


Before scripting, figure out your parameters for the automation:

  • Number and sizes of Virtual Machines (VMs)
  • Image types (Windows, Linux, custom images)
  • Network setup (VNet, Subnets)
  • Bastion configuration
  • Any artifacts (pre-installed tools) required for VMs

My preference is using Terraform for the Infrastructure-as-Code (IaC) feel.

  • Azure CLI (Shell or PowerShell)
  • Azure PowerShell
  • Terraform (if you prefer Infrastructure-as-Code)
  • Azure Bicep (simplified ARM templates)
  • ARM Templates (declarative JSON-based)
  • Python SDK (for more dynamic logic)

For automation, Terraform or Bicep is recommended.


In this section, I’ll overview each prerequisite and configurations needed to deploy an Azure DevTest Lab setup with bastion enabled.

  1. Install Terraform in Azure Cloud Shell & Authenticate to Azure
  2. Create a Terraform Configuration File, main.tf
  3. Create the outputs.tf file
  4. Create a variables.tf file
  5. Create the Execution Plan

Before jump into the code, we need Terraform installed. Walk through Azure’s installation instructions.

A. Install Terraform in the Azure Cloud Shell & Authenticate to Azure

Follow the Azure Cloud Shell instructions in your Azure Portal or the shell script for your preferred OS. See options that OS Microsoft Azure provides. in your Azure Portal.

Notice in the below shell, we have some work to do to update our terminal. So update the shell and then continue with running the commands to login.

Alternatively, you can run a bash or powershell terminal locally. I’m running this on my Mac.

B. Initialize Terraform

terraform init --upgrade

C. Create a Terraform Configuration File (main.tf) (this is the Imperative Approach, where we define the end result first)

terraform {
  required_version = ">=0.12"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "devtestlab_rg" {
  name     = "devtestlab-rg"
  location = "East US"
}

resource "azurerm_dev_test_lab" "devtestlab" {
  name                = "my-devtest-lab"
  resource_group_name = azurerm_resource_group.devtestlab_rg.name
  location            = azurerm_resource_group.devtestlab_rg.location
}

resource "azurerm_dev_test_virtual_network" "vnet" {
  name                = "devtest-vnet"
  resource_group_name = azurerm_resource_group.devtestlab_rg.name
  location            = azurerm_resource_group.devtestlab_rg.location
  lab_name            = azurerm_dev_test_lab.devtestlab.name
  subnet {
    name       = "default"
    use_public_ip_address = false
  }
}

resource "azurerm_bastion_host" "bastion" {
  name                = "my-bastion"
  resource_group_name = azurerm_resource_group.devtestlab_rg.name
  location            = azurerm_resource_group.devtestlab_rg.location
  sku                 = "Standard"

  ip_configuration {
    name                 = "bastion-ipconfig"
    subnet_id            = azurerm_dev_test_virtual_network.vnet.subnet[0].id
    public_ip_address_id = azurerm_public_ip.bastion_ip.id
  }
}

resource "azurerm_dev_test_virtual_machine" "vm" {
  name                = "devtest-vm"
  lab_name            = azurerm_dev_test_lab.devtestlab.name
  resource_group_name = azurerm_resource_group.devtestlab_rg.name
  location            = azurerm_resource_group.devtestlab_rg.location
  size                = "Standard_D2s_v3"
  allow_claim         = true

  storage_os_disk {
    disk_size_gb = 127
    disk_type    = "Standard"
  }

  network_interface {
    use_public_ip_address = true
    subnet_id             = azurerm_dev_test_virtual_network.vnet.subnet[0].id
  }

  os_type   = "Windows"
  username  = "adminuser"
  password  = "YourSecurePassword123!"
}

D. Create a file called outputs.tf, which will store the results for the executed Terrform script above.

output "resource_group_name" {
  value = azurerm_resource_group.rg.name
}

output "lab_name" {
  value = azurerm_dev_test_lab.devtestlab.name
}

output "vm_name" {
  value = azurerm_dev_test_windows_virtual_machine.vm.name
}

output "password" {
  sensitive = true
  value     = local.password
}

E. Next, create a variables.tf file to hold all lab names, resource specs…etc.:

variable "resource_group_location" {
  type        = string
  default     = "eastus"
  description = "Location for all resources."
}

variable "resource_group_name_prefix" {
  type        = string
  default     = "rg"
  description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription."
}

variable "lab_name" {
  type        = string
  description = "My New Awesome DevTest Lab"
  default     = "AwesomeLab"
}

variable "vm_size" {
  type        = string
  description = "The size of the vm to be created."
  default     = "Standard_D4_v3"
}

variable "user_name" {
  type        = string
  description = "The username for the local account that will be created on the new vm."
  default     = "myuser"
}

variable "password" {
  type        = string
  description = "The password for the local account that will be created on the new vm."
  sensitive   = true
  default     = "MySuperSecretPasword123"
}

F. Create the Execution Plan

The Terraform plan command (terraform plan) creates an execution plan to preview your changes when testing your deployment script. In your directory, run this command to produce:

  • Read the current state of your objects, making sure all is updated
  • Compared current and previous state configurations
  • Produces a set of change actions for improving your configuration
  • The -out parameter allows you to create the execution plan, but not actually run it
terraform plan -out main.tfplan

A. Create a Resource Group

az group create --name devtestlab-rg --location eastus

B. Create the DevTest Lab

az lab create --resource-group devtestlab-rg --name my-devtest-lab

C. Create a Virtual Network

az network vnet create --name devtest-vnet --resource-group devtestlab-rg --subnet-name default

D. Deploy Bastion

az network public-ip create --name bastion-ip --resource-group devtestlab-rg --sku Standard az network bastion create --name my-bastion --resource-group devtestlab-rg --location eastus --public-ip-address bastion-ip --vnet-name devtest-vnet

E. Deploy a VM

az lab vm create --lab-name my-devtest-lab --resource-group devtestlab-rg --name devtest-vm --image "Windows Server 2019 Datacenter" --size Standard_D2s_v3 --admin-username adminuser --admin-password "YourSecurePassword123!"

To ensure repeatability for future uses:

  1. For Terraform, use CI/CD (GitHub Actions, Azure DevOps)
  2. For Azure CLI, create a Bash or PowerShell script
  3. For Bicep, use
az deployment group create --template-file main.bicep

5. Verification

Check deployment via Azure Portal (portal.azure.com)

# List resources
az resource list --resource-group devtestlab-rg

Connect to the VM via Bastion


6. Cleanup (If Needed)

To delete the resources:

az group delete --name devtestlab-rg --yes --no-wait

Or with Terraform:

terraform destroy -auto-approve

Conclusion

The best way depends on your preference. This post provided a base example of deploying Azure DevTest Labs using the imperative approach where we define the end result first:

  • Terraform → If you want reusable infrastructure as code.
  • Azure CLI → If you prefer command-line scripting.
  • Bicep/ARM Templates → If you like declarative templates.