Terraform module for creating and managing Azure Virtual Machines (Linux and Windows) with support for managed disks, public IPs, managed identities, and optional diagnostic settings.
This module supports both Linux and Windows VMs through a unified interface, with SSH key authentication for Linux and password authentication for both platforms.
- Dual OS Support: Create Linux or Windows VMs from a single module
- SSH Key Authentication: SSH public key support for Linux VMs
- Password Authentication: Secure password handling via separate variable
- Public IP Management: Optional automatic public IP creation
- Managed Disks: Multiple data disk attachments with configurable storage types
- Managed Identity: SystemAssigned, UserAssigned, or combined identity support
- Security Types: Standard, TrustedLaunch, or ConfidentialVM security configurations
- Custom Images: Marketplace image references with publisher/offer/sku/version
- OS Disk Configuration: Customizable caching, storage type, and disk size
- Diagnostic Settings: Optional Azure Monitor integration
- Resource Group Flexibility: Create new or use existing resource groups
- Tagging Strategy: Built-in default tagging with custom tag support
A simple Linux VM for development with SSH key authentication.
module "linux_vm" {
source = "./modules/vm"
name = "mycompany-dev-aue-app"
resource_group = {
create = false
name = "rg-mycompany-dev-aue-app-001"
location = "australiaeast"
}
tags = {
project = "my-app"
environment = "development"
}
network = {
subnet_id = "/subscriptions/xxxx/resourceGroups/rg-network/providers/Microsoft.Network/virtualNetworks/vnet-dev/subnets/snet-app"
create_public_ip = false
}
vm = {
os_type = "linux"
size = "Standard_B2s"
admin_username = "azureadmin"
disable_password_authentication = true
ssh_public_key = file("~/.ssh/id_rsa.pub")
image = {
publisher = "Canonical"
offer = "ubuntu-24_04-lts"
sku = "server"
version = "latest"
}
os_disk = {
storage_account_type = "Standard_LRS"
disk_size_gb = 30
}
}
}A production Windows VM with managed identity, data disks, and diagnostics.
module "windows_vm" {
source = "./modules/vm"
name = "contoso-prod-aue-web"
resource_group = {
create = false
name = "rg-contoso-prod-aue-web-001"
location = "australiaeast"
}
tags = {
project = "web-platform"
environment = "production"
compliance = "soc2"
}
network = {
subnet_id = "/subscriptions/xxxx/resourceGroups/rg-network/providers/Microsoft.Network/virtualNetworks/vnet-prod/subnets/snet-web"
private_ip_address_allocation = "Static"
private_ip_address = "10.0.1.10"
create_public_ip = false
}
vm = {
os_type = "windows"
size = "Standard_D4s_v5"
admin_username = "winadmin"
image = {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-datacenter-g2"
version = "latest"
}
os_disk = {
storage_account_type = "Premium_LRS"
caching = "ReadWrite"
disk_size_gb = 128
}
identity = {
type = "SystemAssigned"
}
data_disks = [
{
lun = 0
size_gb = 256
storage_account_type = "Premium_LRS"
caching = "ReadOnly"
},
{
lun = 1
size_gb = 512
storage_account_type = "Premium_LRS"
caching = "None"
}
]
}
admin_password = module.vm_password.value
diagnostics = {
enabled = true
log_analytics_workspace_id = "/subscriptions/xxxx/resourceGroups/rg-monitor/providers/Microsoft.OperationalInsights/workspaces/law-prod"
}
}Create a vars/identity.yaml file:
azure:
subscription_id: "afb35bd4-145f-4a15-889e-5da052d030ce"
location: australiaeast
network_lookup:
resource_group_name: "rg-managed-services-lab-aue-stg-001"
vnet_name: "vnet-managed-services-lab-aue-stg-001"
identity:
vms:
app-server:
naming:
org: managed-services
env: lab
region: aue
workload: stg
resource_group:
create: false
name: rg-managed-services-lab-aue-stg-001
location: australiaeast
subnet_name: snet-stg-app
vm:
os_type: linux
size: Standard_B2s
admin_username: azureadmin
disable_password_authentication: false
image:
publisher: Canonical
offer: ubuntu-24_04-lts
sku: server
version: latest
os_disk:
storage_account_type: Standard_LRS
disk_size_gb: 30
identity:
type: SystemAssigned
password:
name: vm-app-server-password
type: password
length: 24Then use in your Terraform:
locals {
workspace = yamldecode(file("vars/${terraform.workspace}.yaml"))
}
data "azurerm_subnet" "vm" {
for_each = try(local.workspace.identity.vms, {})
name = each.value.subnet_name
virtual_network_name = local.workspace.network_lookup.vnet_name
resource_group_name = local.workspace.network_lookup.resource_group_name
}
module "vm_passwords" {
for_each = { for k, v in try(local.workspace.identity.vms, {}) : k => v if try(v.password, null) != null }
source = "./modules/password"
name = each.value.password.name
key_vault_name = module.keyvault["main"].key_vault.name
key_vault_resource_group = module.keyvault["main"].resource_group_name
type = try(each.value.password.type, "password")
length = try(each.value.password.length, 32)
}
module "vm" {
for_each = try(local.workspace.identity.vms, {})
source = "./modules/vm"
name = "${each.value.naming.org}-${each.value.naming.env}-${each.value.naming.region}-${each.value.naming.workload}"
resource_group = each.value.resource_group
tags = try(each.value.tags, {})
network = {
subnet_id = data.azurerm_subnet.vm[each.key].id
}
vm = each.value.vm
admin_password = try(module.vm_passwords[each.key].value, null)
vm_ssh_key = try(module.vm_ssh_keys[each.key].public_key, null)
diagnostics = try(each.value.diagnostics, {})
}| Distribution | Publisher | Offer | SKU |
|---|---|---|---|
| Ubuntu 24.04 | Canonical |
ubuntu-24_04-lts |
server |
| Ubuntu 22.04 | Canonical |
0001-com-ubuntu-server-jammy |
22_04-lts-gen2 |
| RHEL 9 | RedHat |
RHEL |
9-lvm-gen2 |
| Debian 12 | Debian |
debian-12 |
12-gen2 |
| Version | Publisher | Offer | SKU |
|---|---|---|---|
| Server 2022 | MicrosoftWindowsServer |
WindowsServer |
2022-datacenter-g2 |
| Server 2019 | MicrosoftWindowsServer |
WindowsServer |
2019-Datacenter |
Resources are named using the prefix pattern: {name}
Example:
- Linux VM:
vm-{name}-001 - Windows VM:
vm-{name}-001 - NIC:
nic-vm-{name}-001 - Public IP:
pip-vm-{name}-001 - OS Disk:
osdisk-vm-{name}-001 - Data Disk:
disk-vm-{name}-lun{N}-001
| Name | Description |
|---|---|
resource_group_name |
Resource Group where the VM is deployed |
vm |
VM object with id, name, os, private/public IPs, identity, data disks |
resource |
Generic resource output (id, name, type) |
nic |
NIC output with id, name, ip_configuration_name (for LB associations) |
| Name | Version |
|---|---|
| terraform | >= 1.6.0 |
| azurerm | >= 4.0.0 |
| Name | Version |
|---|---|
| azurerm | >= 4.0.0 |
| Name | Description | Type | Required |
|---|---|---|---|
name |
Resource name prefix for all resources | string | yes |
resource_group |
Resource group configuration | object | yes |
network |
Network settings for the VM NIC | object | yes |
vm |
VM configuration (OS type, size, image, disks) | object | yes |
tags |
Extra tags merged with default tags | map(string) | no |
admin_password |
VM admin password (sensitive) | string | no |
vm_ssh_key |
SSH public key override | string | no |
diagnostics |
Azure Monitor diagnostic settings | object | no |
object({
os_type = string # linux | windows
name = optional(string)
name_suffix = optional(string, "001")
size = string # e.g., Standard_B2s, Standard_D4s_v5
admin_username = string
disable_password_authentication = optional(bool, true) # Linux only
ssh_public_key = optional(string) # Linux only
computer_name = optional(string)
image = object({
publisher = string
offer = string
sku = string
version = optional(string, "latest")
})
os_disk = optional(object({
name = optional(string)
caching = optional(string, "ReadWrite")
storage_account_type = optional(string, "Standard_LRS")
disk_size_gb = optional(number)
}), {})
identity = optional(object({
type = optional(string, "SystemAssigned")
identity_ids = optional(list(string), [])
}), {})
security_type = optional(string)
secure_boot_enabled = optional(bool, false)
vtpm_enabled = optional(bool, false)
data_disks = optional(list(object({
lun = number
size_gb = number
storage_account_type = optional(string, "Standard_LRS")
caching = optional(string, "ReadOnly")
create_option = optional(string, "Empty")
name = optional(string)
})), [])
})object({
subnet_id = string
private_ip_address_allocation = optional(string, "Dynamic")
private_ip_address = optional(string)
create_public_ip = optional(bool, false)
public_ip_sku = optional(string, "Standard")
public_ip_allocation_method = optional(string, "Static")
})Apache 2.0 Licensed. See LICENSE for full details.
Module managed by DNX Solutions.
Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.