
Deploying Virtual Machines into Azure – My Attempt at a Simplified VM Module
Have you ever needed to deploy a set of virtual machines or a single virtual machine into Azure across multiple environments with Terraform? Did you too look to existing Microsoft created or community created modules to accomplish this and find that the existing modules are overly complicated and kind of frustrating to work with?
The Problem with Existing Azure VM Modules
You might have come across the Azure Verified Module (AVM) for virtual machines or the older Azure VM Terraform module. Both modules are good but they are complicated and they do not deploy more than one virtual machine without multiple module blocks in your configuration file.
The Azure Verified Module (AVM)
The newer terraform-azurerm-avm-res-compute-virtualmachine is thorough but introduces complexity with:
- Extensive configuration options that most deployments don’t need
- Nearly 200 configuration parameters (most of which you’ll never use)
- Can only deploy a single VM per module call
- Documentation spanning multiple files
- A learning curve that slows down development
The Legacy Azure VM Module
The older terraform-azurerm-vm module has its own problems:
- Outdated defaults for VM sizes and OS images
- Clunky syntax for configuring VMs
- Designed for deploying single VMs
Introducing a Simpler Approach
After wrestling with these modules, I created my own module that focuses on what most of us actually need: a simple way to deploy multiple VMs (both Windows and Linux) in one go across multiple environments.
My module provides:
- Simple configuration – Use a map structure that’s easy to understand and maintain
- Key Vault integration – Secure password handling via Azure Key Vault
- Sensible defaults – Modern VM images and configurations out of the box, can easily be changed. One for Windows and one for Linux
- Reduced complexity – Only the options you’ll actually use in the majority of use cases
When to Use This Module
This module is perfect for:
- Multiple VM Deployments: Deploy any number of Windows and Linux VMs with a single module call.
- Multi-Environment Infrastructure: If you need to manage different configurations for dev, test, and production environments.
- Mixed OS Requirements: Create both Windows and Linux VMs in the same deployment
- Handoff-Ready Deployments: Create base VMs that can be handed off to Sysadmins teams to perform specialized configurations like domain joining, data disk management, and application setup.
When Not to Use This Module
While this module works well deploying multiple VMs with basic configurations, it’s not suitable for all scenarios. Consider alternatives if you need the below in your IaC configuration. In my case, I have no problems using this module to create the initial VMs and then handing it off to its final VM owner group to add all missing resources / extensions the implementation requires.
- VMs with Data Disks: This module doesn’t support attaching additional data disks. If your workloads require separate data volumes, you’ll need a different approach.
- Linux VMs with SSH Key Authentication: Currently, the module only supports password authentication for Linux VMs. If SSH key-based authentication is required for your security policies, you’ll need to use another solution.
- Domain-Joined Windows VMs: The module doesn’t include domain join capabilities for Windows VMs. If Active Directory integration is needed, consider using additional custom scripts or a more specialized module.
- Network Security Groups (NSGs): This module intentionally doesn’t create or manage NSGs, as we consider network security to be better handled at the subnet level rather than per VM. If you need NSGs tightly coupled with VM deployments, this module isn’t the right fit.
- Advanced Networking Configurations: If you need complex networking setups beyond basic subnet attachment and optional public IPs, you might need to customize this module or use the Azure Verified Module instead.
- Highly Customized VM Extensions: While this module focuses on simplicity, scenarios requiring multiple custom VM extensions might be better served by a more complex solution.
For these advanced scenarios, consider using the Azure Verified Module or creating your own specialized module.
How It Works
The module uses a straightforward map structure where each key-value pair represents a VM with its own configuration:
vms = {
"win-vm-1" = {
name = "win-vm-1"
image_os = "windows"
size = "Standard_D2s_v3"
admin_password_secret_name = "win-vm-1-password"
os_disk_name = "win-vm-1-osdisk"
subnet_id = "/subscriptions/.../subnets/example-subnet"
is_public = true # Optional: Set to true to create a public IP
},
"linux-vm-1" = {
name = "linux-vm-1"
image_os = "linux"
size = "Standard_D2s_v3"
admin_password_secret_name = "linux-vm-1-password"
os_disk_name = "linux-vm-1-osdisk"
subnet_id = "/subscriptions/.../subnets/example-subnet"
is_public = false # This VM will only have a private IP
}
# Add as many VMs as you need...
}
The module handles everything else:
- Creates the appropriate VM type based on the OS
- Configures network interfaces for each VM
- Sets up public IPs only for VMs where you specify
is_public = true
- Integrates with your existing virtual network
- Retrieves admin passwords securely from your Key Vault
Example
Here’s a complete example showing how to deploy multiple VMs across your environment:
Main Configuration (main.tf)
module "multiple_vms" {
source = "path/to/module"
# Base configuration
subscription_id = "your-subscription-id"
location = "eastus"
resource_group_name = "example-rg"
key_vault_name = "example-kv"
key_vault_resource_group_name = "example-kv-rg"
# Admin and disk settings
vm_admin_username = "azureadmin"
os_disk_caching = "ReadWrite"
os_disk_storage_account_type = "Premium_LRS"
os_disk_size_gb = 128
vm_identity_type = "SystemAssigned"
}
Environment-Specific Configuration (dev.tfvars)
# Optional: Availability set configuration
# availability_set_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-rg/providers/Microsoft.Compute/availabilitySets/example-avset"
# Image configurations
source_image_publisher_windows = "MicrosoftWindowsServer"
source_image_offer_windows = "WindowsServer"
source_image_sku_windows = "2022-Datacenter"
source_image_version_windows = "latest"
source_image_publisher_linux = "RedHat"
source_image_offer_linux = "RHEL"
source_image_sku_linux = "93-gen2"
source_image_version_linux = "latest"
# VM definitions
vms = {
"win-vm-1" = {
name = "win-vm-1"
image_os = "windows"
size = "Standard_D2s_v3"
admin_password_secret_name = "win-vm-1-password"
os_disk_name = "win-vm-1-osdisk"
subnet_id = "/subscriptions/.../example-subnet"
is_public = true # This VM will get a public IP
},
"win-vm-2" = {
name = "win-vm-2"
image_os = "windows"
size = "Standard_D4s_v3"
admin_password_secret_name = "win-vm-2-password"
os_disk_name = "win-vm-2-osdisk"
subnet_id = "/subscriptions/.../example-subnet"
is_public = false # This VM will only have a private IP
},
"linux-vm-1" = {
name = "linux-vm-1"
image_os = "linux"
size = "Standard_D2s_v3"
admin_password_secret_name = "linux-vm-1-password"
os_disk_name = "linux-vm-1-osdisk"
subnet_id = "/subscriptions/.../example-subnet"
is_public = true # This VM will get a public IP
}
}
Repository Structure
I recommend this simple directory structure for your deployment:
azure-vm-infrastructure/
│
├── main.tf # Main Terraform configuration with module reference
├── variables.tf # Variable declarations
│
├── dev.tfvars # Development environment configuration
├── qa.tfvars # QA environment configuration
└── prod.tfvars # Production environment configuration
How to Deploy
Deploying to different environments is as simple as specifying the right variable file:
# Development environment
terraform apply -var-file="dev.tfvars"
# QA environment
terraform apply -var-file="qa.tfvars"
# Production environment
terraform apply -var-file="prod.tfvars"
This approach lets you maintain consistent infrastructure definitions while tailoring specific settings for each environment. Each .tfvars file can contain different VM sizes, quantities, and configurations appropriate for that environment.
Note: if deploying this in a local directory you will need to clear the terraform generated files such as state and .terraform directory before attempting to run terraform apply against any additional environments.
For complete examples and documentation, check out the example-config
and/or test-deployment directories in the module repository.
To Sum Up
Sometimes, simpler is better. While the Microsoft-provided modules offer lots of customization, they often add too much complexity (IMO) for common deployment scenarios.
Try it for your next Azure VM deployment!
If you find this useful or have suggestions for improvements please let me know.
Thanks for reading.
Related Links:
https://registry.terraform.io/modules/RCFromCLE/virtual-machine-multiple/azurerm/latest
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/windows_virtual_machine