Terraform module for deploying multiple Azure VMs
Blog post - Deploying Virtual Machines Into Azure - My Simplified Terraform Module
Features:
- Windows and Linux VMs in a single deployment
- VM-specific public IP configuration (optional per VM)
- Key Vault integration for password management
- Availability set support
- Default images: Windows Server 2022 and RHEL 9.3-gen2
Deploy multiple Windows and Linux VMs with shared base settings and VM-specific configurations using a map structure.
Required Base Configuration:
| Variable | Description |
|---|---|
subscription_id |
Azure subscription ID |
resource_group_name |
Resource group name for VM deployment |
location |
Azure region (e.g., 'eastus') |
key_vault_name |
Key Vault containing VM passwords |
key_vault_resource_group_name |
Resource group for Key Vault |
vm_admin_username |
Admin username for all VMs |
os_disk_caching |
OS disk caching type ('None', 'ReadOnly', 'ReadWrite') |
os_disk_storage_account_type |
OS disk storage type ('Standard_LRS', 'Premium_LRS') |
os_disk_size_gb |
OS disk size in GB (min 128 GB for Windows) |
vm_identity_type |
VM identity type ('SystemAssigned', 'UserAssigned', or both) |
VM Map Configuration:
Each VM in the vms map requires:
| Variable | Description | Required |
|---|---|---|
name |
VM name | Yes |
image_os |
OS type ('windows' or 'linux') | Yes |
size |
VM size (e.g., 'Standard_D2s_v3') | Yes |
admin_password_secret_name |
Key Vault secret name for VM password | Yes |
os_disk_name |
OS disk name | Yes |
subnet_id |
Subnet ID for VM deployment | Yes |
is_public |
Create public IP for this VM | No (default: false) |
Optional Variables:
| Variable | Description | Default |
|---|---|---|
availability_set_id |
Availability set ID | null |
source_image_publisher_windows |
Windows image publisher | 'MicrosoftWindowsServer' |
source_image_offer_windows |
Windows image offer | 'WindowsServer' |
source_image_sku_windows |
Windows image SKU | '2022-Datacenter' |
source_image_version_windows |
Windows image version | 'latest' |
source_image_publisher_linux |
Linux image publisher | 'RedHat' |
source_image_offer_linux |
Linux image offer | 'RHEL' |
source_image_sku_linux |
Linux image SKU | '93-gen2' |
source_image_version_linux |
Linux image version | 'latest' |
main.tf:
module "multiple_vms" {
source = "path/to/module"
# Base configuration
subscription_id = var.subscription_id
location = var.location
resource_group_name = var.resource_group_name
key_vault_name = var.key_vault_name
key_vault_resource_group_name = var.key_vault_resource_group_name
vm_admin_username = var.vm_admin_username
os_disk_caching = var.os_disk_caching
os_disk_storage_account_type = var.os_disk_storage_account_type
os_disk_size_gb = var.os_disk_size_gb
vm_identity_type = var.vm_identity_type
# VM configurations from tfvars file
vms = var.vms
}terraform.tfvars:
# Base configuration
subscription_id = "00000000-0000-0000-0000-000000000000"
location = "eastus"
resource_group_name = "example-rg"
key_vault_name = "example-kv"
key_vault_resource_group_name = "example-kv-rg"
vm_admin_username = "azureadmin"
os_disk_caching = "ReadWrite"
os_disk_storage_account_type = "Premium_LRS"
os_disk_size_gb = 128
vm_identity_type = "SystemAssigned"
# VM configurations
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 # Creates 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 defaults to false - No public IP
}
}See examples.tf for a comprehensive example with 5 VMs (3 Windows, 2 Linux) with varied configurations.
Use the same main.tf with different environment-specific .tfvars files:
main.tf (same as above, referencing variables)
dev.tfvars (development environment):
# Base configuration for development
subscription_id = "dev-subscription-id"
location = "eastus"
resource_group_name = "dev-rg"
key_vault_name = "dev-kv"
key_vault_resource_group_name = "dev-kv-rg"
# Other dev settings...
# VM configurations for development
vms = {
"dev-vm-1" = {
name = "dev-vm-1"
image_os = "windows"
size = "Standard_D2s_v3"
admin_password_secret_name = "dev-vm-1-password"
os_disk_name = "dev-vm-1-osdisk"
subnet_id = "/subscriptions/.../dev-subnet"
is_public = true # Public IP for development testing
}
}prod.tfvars (production environment):
# Base configuration for production
subscription_id = "prod-subscription-id"
location = "westus"
resource_group_name = "prod-rg"
# Other production settings...
# VM configurations for production
vms = {
"prod-vm-1" = {
name = "prod-vm-1"
image_os = "windows"
size = "Standard_D4s_v3"
admin_password_secret_name = "prod-vm-1-password"
os_disk_name = "prod-vm-1-osdisk"
subnet_id = "/subscriptions/.../prod-subnet"
is_public = false # No public IPs in production
}
}Apply with: terraform apply -var-file="[environment].tfvars"