Skip to content

libre-devops/terraform-azurerm-linux-vm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

63 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Terraform Azure Linux VM

Hardened Linux VMs by default: SSH-only, Trusted Launch, managed identity, a verified image catalog, and VM Insights wired in one attribute.

CI Release Terraform Registry License


Overview

VMs keyed by name, each with its own NIC, built so the SECURE shape is the ZERO-CONFIG shape:

  • SSH-only authentication (passwords are an explicit, plan-visible opt-in).
  • Trusted Launch (secure boot + vTPM) on by default.
  • A system-assigned managed identity on every VM (what the monitor agent and RBAC grants use).
  • Managed boot diagnostics and platform patch assessment on by default.
  • Public IPs are inputs only (they live in the public-ip module), and attaching one is plan-visible; the intended door is the bastion module's free Developer SKU.

The image catalog replaces the old external "SKU calculator" modules: source_image_simple takes a friendly key (Ubuntu2204, Ubuntu2404, Debian12, RHEL9, Sles15, Rocky9; discover them via the image_catalog_keys output) resolving to marketplace references verified against the live platform, all Gen2 and Trusted Launch capable; Rocky carries its marketplace plan automatically. source_image_reference and source_image_id remain first-class, and marketplace plan images get their azurerm_marketplace_agreement created (deduplicated) when accept_marketplace_agreement = true.

VM Insights in one attribute: vm_insights = { log_analytics_workspace_id = ... } installs the Azure Monitor agent on every VM (per-VM opt-out), creates the VM Insights data collection rule (or associates an existing data_collection_rule_id), and associates each VM, using the VMs' managed identities.

The rest of the surface: flexible identity (SystemAssigned by default, UserAssigned, both, or None), a cloud_init convenience that base64-encodes plain cloud-init YAML for you (the often-forgotten step), data disks with auto-assigned LUNs, spot pricing, zones and every placement option (availability sets, VMSS attachment, proximity groups, capacity reservations, dedicated hosts), ASG associations, static private IPs, accelerated networking, encryption at host, gallery applications, key vault certificate secrets, termination and OS image notifications, modern run commands, and full patching controls. The often-forgotten subscription plumbing is opt-in and conditional too: resource_provider_feature_registrations (for example Microsoft.Compute/EncryptionAtHost, which a check reminds you about) and resource_provider_registrations, both documented as subscription-wide and single-owner.

Disclaimers (from the provider, worth knowing)

  • Terraform removes the OS disk with the VM by default (configurable via the provider features block).
  • All arguments, including the administrator login and password, are stored in the raw state as plain text: protect the state.
  • azurerm_linux_virtual_machine does not support unmanaged disks or attaching existing OS disks (capture an image, or fall back to azurerm_virtual_machine).
  • public_ip_address outputs may be unpopulated for Dynamic public IPs.
  • vm_agent_platform_updates_enabled is platform-controlled and read-only; this module does not touch it.

Usage

module "linux_vm" {
  source  = "libre-devops/linux-vm/azurerm"
  version = "~> 4.0"

  resource_group_id = module.rg.ids["rg-ldo-uks-prd-001"]
  location          = "uksouth"
  tags              = module.tags.tags

  vm_insights = {
    log_analytics_workspace_id = module.log_analytics.workspace_ids["log-ldo-uks-prd-001"]
  }

  linux_virtual_machines = {
    "vm-ldo-app-uks-prd-001" = {
      size                = "Standard_D2s_v5"
      admin_username      = "azureuser"
      source_image_simple = "Ubuntu2404"
      subnet_id           = module.network.subnet_ids["snet-app"]
      admin_ssh_keys = [{
        public_key = module.ssh_key.public_keys_openssh["ssh-ldo-uks-prd-001"]
      }]
      data_disks = {
        "datadisk01-vm-ldo-app-uks-prd-001" = { disk_size_gb = 128 }
      }
    }
  }
}

Examples

  • examples/minimal - one VM with the secure defaults from a catalog image and a bring-your-own key.
  • examples/complete - the "secure VM estate in a pinch" build: tags, rg, vnet, forward and reverse private DNS with auto-registration, a free Developer bastion as the door, a firewalled vault holding ephemerally generated SSH keys (write-only, never in state), Log Analytics with VM Insights on every VM, and two hardened VMs exercising the full surface (catalog and explicit images, data disks, spot, zones, static IPs, accelerated networking, run commands).

Developing

Local work needs PowerShell 7+ and just, because the recipes wrap the LibreDevOpsHelpers PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install just with brew install just, or uv tool add rust-just then uv run just <recipe>.

Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only), just plan, just apply, just destroy, just e2e, just test, and just docs (the plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e applies an example then always destroys it, defaulting to minimal, so nothing is left running). Releasing is also just: just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the Terraform Registry picks up the tag.

Security scan exceptions

This module is scanned with Trivy; HIGH and CRITICAL findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a finding that should be fixed. Waivers live in .trivyignore.yaml (the machine-applied source of truth, passed to Trivy with --ignorefile) and are mirrored in a table here so the reason is auditable.

ID Scope Reason
AVD-AZU-0013 (vault network ACL default action) examples/complete/main.tf The disposable example vault opts out of the keyvault module's deny-by-default firewall so the CI self-test runner can reach the data plane; real deployments keep the secure default, and the firewalled shape plus the terraform-azure action's key vault dance are documented alongside.

To add an exception: add an entry to .trivyignore.yaml (id, optional paths to scope it, and a statement recording why), then add a matching row here recording the reason. Both the file and the table are reviewed in the pull request.

Reference

The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.

Requirements

Name Version
terraform >= 1.9.0, < 2.0.0
azurerm >= 4.0.0, < 5.0.0

Providers

Name Version
azurerm >= 4.0.0, < 5.0.0

Modules

No modules.

Resources

Name Type
azurerm_linux_virtual_machine.this resource
azurerm_managed_disk.data resource
azurerm_marketplace_agreement.this resource
azurerm_monitor_data_collection_rule.vm_insights resource
azurerm_monitor_data_collection_rule_association.vm_insights resource
azurerm_network_interface.this resource
azurerm_network_interface_application_security_group_association.this resource
azurerm_resource_provider_feature_registration.this resource
azurerm_resource_provider_registration.this resource
azurerm_virtual_machine_data_disk_attachment.data resource
azurerm_virtual_machine_extension.monitor_agent resource
azurerm_virtual_machine_run_command.this resource

Inputs

Name Description Type Default Required
linux_virtual_machines The Linux VMs to create, keyed by VM name. Each VM gets its own NIC (subnet_id is where it lives;
public_ip_address_id is ONLY an input, public IPs live in the public-ip module).

SECURE DEFAULTS: SSH-only authentication (disable_password_authentication = true, so admin_ssh_keys
is required unless you explicitly enable passwords), Trusted Launch (secure_boot_enabled and
vtpm_enabled true; the image catalog entries are all Gen2 and Trusted Launch capable), a
system-assigned managed identity, managed boot diagnostics, and platform patch assessment.

IMAGE SELECTION, exactly one of:
- source_image_simple: a friendly catalog key (Ubuntu2204, Ubuntu2404, Debian12, RHEL9, Sles15,
Rocky9; see the image_catalog_keys output), verified Gen2/Trusted Launch marketplace references.
Rocky carries its marketplace plan automatically (accept with accept_marketplace_agreement).
- source_image_reference: { publisher, offer, sku, version (default latest) } for anything else.
- source_image_id: a custom or gallery image id.
Marketplace plan images: set plan { name, product, publisher } and optionally
accept_marketplace_agreement = true to create the azurerm_marketplace_agreement.

NETWORKING per VM: subnet_id (required), private_ip_address (static when set), public_ip_address_id,
accelerated_networking_enabled (default false; not every size supports it), ip_forwarding_enabled,
dns_servers, application_security_group_ids (associations only; ASGs live with the network modules),
nic_name / ipconfig_name overrides.

DISKS: os_disk (caching ReadWrite, StandardSSD_LRS by default, plus size, encryption set, security
encryption, write accelerator, diff_disk_settings) and data_disks keyed by name (size_gb required;
lun auto-assigned by declaration order unless set; storage_account_type, caching, create_option,
encryption set, zone follows the VM).

EVERYTHING ELSE: zone, availability_set_id, virtual_machine_scale_set_id, proximity_placement_group_id,
capacity_reservation_group_id, dedicated_host_id / dedicated_host_group_id, platform_fault_domain,
edge_zone; spot { max_bid_price, eviction_policy }; additional_capabilities (ultra SSD, hibernation);
encryption_at_host_enabled (subscription feature-gated; see resource_provider_feature_registrations
for the often-forgotten registration); identity (SystemAssigned by default, UserAssigned, both as
"SystemAssigned, UserAssigned", or None for no identity at all); patching
(patch_mode default ImageDefault, patch_assessment_mode default AutomaticByPlatform, reboot_setting,
bypass flag); license_type; user_data / custom_data; computer_name (defaults from the VM name);
disk_controller_type; extensions_time_budget; gallery_applications; secrets (key vault certificates);
termination_notification; os_image_notification; boot_diagnostics_storage_account_uri (unset =
managed storage); run_command { script | script_uri | command_id, run_as_user, run_as_password };
cloud_init (plain cloud-init YAML, base64-encoded for you; mutually exclusive with custom_data,
which passes through pre-encoded); monitor_agent_enabled (default true, only relevant when
vm_insights is set) and per-VM tags.

PROVIDER DISCLAIMERS worth knowing: the OS disk is deleted with the VM by default (configurable via
the provider features block); all arguments including admin credentials are stored in the raw state
as plain text (protect the state); unmanaged disks and attaching existing OS disks are not supported
by azurerm_linux_virtual_machine; and public_ip_address outputs may be unpopulated for Dynamic
public IPs.
map(object({
size = string
admin_username = string

admin_ssh_keys = optional(list(object({
public_key = string
username = optional(string)
})), [])
admin_password = optional(string)
disable_password_authentication = optional(bool, true)

source_image_simple = optional(string)
source_image_reference = optional(object({
publisher = string
offer = string
sku = string
version = optional(string, "latest")
}))
source_image_id = optional(string)
plan = optional(object({
name = string
product = string
publisher = string
}))
accept_marketplace_agreement = optional(bool, false)

subnet_id = string
private_ip_address = optional(string)
public_ip_address_id = optional(string)
accelerated_networking_enabled = optional(bool, false)
ip_forwarding_enabled = optional(bool, false)
dns_servers = optional(list(string))
application_security_group_ids = optional(list(string), [])
nic_name = optional(string)
ipconfig_name = optional(string)

os_disk = optional(object({
name = optional(string)
caching = optional(string, "ReadWrite")
storage_account_type = optional(string, "StandardSSD_LRS")
disk_size_gb = optional(number)
disk_encryption_set_id = optional(string)
secure_vm_disk_encryption_set_id = optional(string)
security_encryption_type = optional(string)
write_accelerator_enabled = optional(bool, false)
diff_disk_settings = optional(object({
option = string
}))
}), {})

data_disks = optional(map(object({
disk_size_gb = number
lun = optional(number)
storage_account_type = optional(string, "StandardSSD_LRS")
caching = optional(string, "ReadWrite")
create_option = optional(string, "Empty")
source_resource_id = optional(string)
disk_encryption_set_id = optional(string)
})), {})

secure_boot_enabled = optional(bool, true)
vtpm_enabled = optional(bool, true)
encryption_at_host_enabled = optional(bool)

identity = optional(object({
type = optional(string, "SystemAssigned")
identity_ids = optional(list(string))
}), {})

boot_diagnostics_enabled = optional(bool, true)
boot_diagnostics_storage_account_uri = optional(string)

patch_mode = optional(string, "ImageDefault")
patch_assessment_mode = optional(string, "AutomaticByPlatform")
bypass_platform_safety_checks_on_user_schedule_enabled = optional(bool, false)
reboot_setting = optional(string)
provision_vm_agent = optional(bool, true)
allow_extension_operations = optional(bool, true)
extensions_time_budget = optional(string)

zone = optional(string)
availability_set_id = optional(string)
virtual_machine_scale_set_id = optional(string)
proximity_placement_group_id = optional(string)
capacity_reservation_group_id = optional(string)
dedicated_host_id = optional(string)
dedicated_host_group_id = optional(string)
platform_fault_domain = optional(number)
edge_zone = optional(string)

spot = optional(object({
max_bid_price = optional(number, -1)
eviction_policy = optional(string, "Deallocate")
}))

additional_capabilities = optional(object({
ultra_ssd_enabled = optional(bool, false)
hibernation_enabled = optional(bool, false)
}))

license_type = optional(string)
user_data = optional(string)
custom_data = optional(string)
cloud_init = optional(string)
computer_name = optional(string)
disk_controller_type = optional(string)

gallery_applications = optional(list(object({
version_id = string
automatic_upgrade_enabled = optional(bool)
configuration_blob_uri = optional(string)
order = optional(number)
tag = optional(string)
treat_failure_as_deployment_failure_enabled = optional(bool)
})), [])

secrets = optional(list(object({
key_vault_id = string
certificate_urls = list(string)
})), [])

termination_notification = optional(object({
enabled = bool
timeout = optional(string)
}))
os_image_notification_timeout = optional(string)

run_command = optional(object({
name = optional(string)
script = optional(string)
script_uri = optional(string)
command_id = optional(string)
run_as_user = optional(string)
run_as_password = optional(string)
}))

monitor_agent_enabled = optional(bool, true)
tags = optional(map(string))
}))
{} no
location Azure region for the VMs. string n/a yes
resource_group_id Resource id of the resource group to create the VMs in. The name is parsed from it (pass the rg module's ids output). string n/a yes
resource_provider_feature_registrations Preview/opt-in features to register, as "Namespace/FeatureName" strings (for example
"Microsoft.Compute/EncryptionAtHost", the classic forgotten prerequisite for
encryption_at_host_enabled). Empty (the default) manages nothing. Registration is subscription-wide
and can take several minutes to propagate.
set(string) [] no
resource_provider_registrations Resource provider namespaces to register on the subscription (for example Microsoft.Monitor), for
subscriptions where the provider has never been used. Empty (the default) manages nothing. NOTE: a
namespace already registered elsewhere will conflict on import/destroy; this is for the deliberate
owner only, and the provider's own resource_provider_registrations setting must not also manage it.
set(string) [] no
tags Tags applied to every resource this module creates (merged with any per-VM tags). map(string) {} no
vm_insights Opt-in VM Insights for every VM in this call: the module installs the Azure Monitor agent (each VM's
system-assigned identity is what the agent authenticates with, which the module enables by default),
creates the VM Insights data collection rule pointed at log_analytics_workspace_id (or associates an
existing one passed as data_collection_rule_id), and associates every VM with it. null (the default)
creates nothing.
object({
log_analytics_workspace_id = optional(string)
data_collection_rule_id = optional(string)
})
null no

Outputs

Name Description
data_collection_rule_id The VM Insights data collection rule in effect (created or passed in); null when vm_insights is off.
data_disk_ids Map of "vm|disk" to managed disk id.
data_disks The data disks, keyed "vm|disk". Full resource objects.
identity_principal_ids Map of VM name to the system-assigned identity principal id (what RBAC assignments target; null when the VM has no system identity).
ids Map of VM name to resource id.
ids_zipmap Map of VM name to { name, id }, for easy composition with other modules.
image_catalog The full image catalog (key => { publisher, offer, sku }), verified Gen2 / Trusted Launch capable marketplace references.
image_catalog_keys Every friendly key the image catalog offers for source_image_simple.
linux_virtual_machines The VMs, keyed by name: every attribute except the provider's deprecated vm_agent_platform_updates_enabled (a full-object output would trip its deprecation warning). Sensitive because admin_password and custom_data are inside.
names Map of VM name to name (convenience passthrough).
network_interface_ids Map of VM name to NIC id.
network_interfaces The NICs, keyed by VM name. Full resource objects.
private_ip_addresses Map of VM name to primary private IP address.
resource_group_name The resource group the VMs live in, parsed from resource_group_id.
virtual_machine_ids Map of VM name to the unique VM id (the compute fabric's GUID, not the resource id).

About

🐧 Hardened-by-default Linux VMs: SSH-only, Trusted Launch, managed identity, a verified image catalog, and VM Insights in one attribute

Topics

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors