Terraform Azure VM Extensions

Having recently gone through getting Terraform to deploy a virtual machine and a VM extension to register Desired State Configuration (DSC) with Azure Automation, I thought I’d note the method and code here for future reference.

This presumes a functioning Azure Automation account with a DSC configuration and generated node configurations.

 

First, I specify my variables in a file “Variables.tf”:

// For all VMs
 
variable "subscription" { type = "string" }
variable "location" { type= "string" }
variable "vmsize" { type = "map" }
variable "username" { type = "string" }
variable "clientcode" { type = "string" }
variable "password" { type = "map" }
variable "networkipaddress" { type = "map" }
variable "serveripaddress" { type = "map" }
variable "dnsservers" { type = "list" }
variable "dcdnsservers" { type = "list" }
//DSC related
# The key for the Azure Automation account
variable "dsc_key" { type = "string" }
# Endpoint, also referred to as the Registration URL
variable "dsc_endpoint" { type = "string" }
# This can be ApplyAndMonitor, ApplyandAutoCorrect, among others
variable "dsc_mode" { type = "string" }
# Heres where you define the node configuration that you actually want to apply to the VM
variable "dsc_nodeconfigname" { type = "map" }
 
variable "dsc_configfrequency" { type = "string" }
variable "dsc_refreshfrequency" { type = "string" }

Then I deploy the VM, along with the required dependencies:

# Initial Resource Group
resource "azurerm_resource_group" "Default" {
  name     = "az${var.clientcode}"
  location = "${var.location}"
}
 
resource "azurerm_virtual_network" "VirtualNetwork" {
  name                = "azcx${var.clientcode}"
  address_space       = ["${lookup(var.networkipaddress, "VMNet")}"]
  location            = "${var.location}"
  resource_group_name = "${azurerm_resource_group.Default.name}"
  dns_servers         = "${var.dnsservers}"
}
 
resource "azurerm_subnet" "subnet-lan" {
  name                 = "az${var.clientcode}-lan"
  resource_group_name  = "${azurerm_resource_group.Default.name}"
  virtual_network_name = "${azurerm_virtual_network.VirtualNetwork.name}"
  address_prefix       = "${lookup(var.networkipaddress, "subnet-lan")}"
}
resource "azurerm_network_interface" "dc1nic1" {
  name                = "az${var.clientcode}1nic1"
  location            = "${var.location}"
  resource_group_name = "${azurerm_resource_group.Default.name}"
  # reverse DNS for the domain controller
  dns_servers = "${var.dcdnsservers}"
 
  ip_configuration {
    name                          = "ipconfig1"
    subnet_id                     = "${azurerm_subnet.subnet-lan.id}"
    private_ip_address_allocation = "static"
    private_ip_address            = "${lookup(var.serveripaddress, "az${var.clientcode}1")}"
 
  }
}
resource "azurerm_virtual_machine" "dc1" {
  name                             = "az${var.clientcode}1"
  location                         = "${var.location}"
  resource_group_name              = "${azurerm_resource_group.Default.name}"
  network_interface_ids            = ["${azurerm_network_interface.dc1nic1.id}"]
  vm_size                          = "${lookup(var.vmsize, "az${var.clientcode}1")}"
  delete_os_disk_on_termination    = true
  delete_data_disks_on_termination = true
 
  storage_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2012-R2-Datacenter"
    version   = "latest"
  }
 
  storage_os_disk {
    name              = "az${var.clientcode}1_c"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
    disk_size_gb      = "128"
  }
 
  storage_data_disk {
    name              = "az${var.clientcode}1_d"
    managed_disk_type = "Standard_LRS"
    caching           = "None"
    create_option     = "Empty"
    lun               = 0
    disk_size_gb      = "64"
  }
 
  os_profile {
    computer_name  = "az${var.clientcode}1"
    admin_username = "${var.username}"
    admin_password = "${lookup(var.password, "az${var.clientcode}1")}"
  }
 
  os_profile_windows_config {
    provision_vm_agent        = true
    enable_automatic_upgrades = false
  }
}

Now with the VM deployed, the Extension can be applied. This will pass in the proper variables defined, install the VM extension, and register with the Azure Automation account to begin the initial DSC deployment. Note, due to WordPress formatting, the line below that says “setting = SETTINGS” should look like this: “setting = <<SETTINGS”. The double bracket should exist for the PROTECTED_SETTINGS line too.

resource "azurerm_virtual_machine_extension" "dc1-dsc" {
  name                 = "Microsoft.Powershell.DSC"
  location             = "${var.location}"
  resource_group_name  = "${azurerm_resource_group.Default.name}"
  virtual_machine_name = "az${var.clientcode}1"
  publisher            = "Microsoft.Powershell"
  type                 = "DSC"
  auto_upgrade_minor_version = true
  type_handler_version = "2.76"
  depends_on           = ["azurerm_virtual_machine.dc1"]
 
  settings = SETTINGS
        {
            "WmfVersion": "latest",
            "advancedOptions": {
	              "forcePullAndApply": true 
                },
            "Properties": {
                "RegistrationKey": {
                  "UserName": "PLACEHOLDER_DONOTUSE",
                  "Password": "PrivateSettingsRef:registrationKeyPrivate"
                },
                "RegistrationUrl": "${var.dsc_endpoint}",
                "NodeConfigurationName": "${lookup(var.dsc_nodeconfigname, "dc1")}",
                "ConfigurationMode": "${var.dsc_mode}",
                "ConfigurationModeFrequencyMins": ${var.dsc_configfrequency},
                "RefreshFrequencyMins": ${var.dsc_refreshfrequency},
                "RebootNodeIfNeeded": true,
                "ActionAfterReboot": "continueConfiguration",
                "AllowModuleOverwrite": true
            }
        }
    SETTINGS
  protected_settings = PROTECTED_SETTINGS
    {
      "Items": {
        "registrationKeyPrivate" : "${var.dsc_key}"
      }
    }
PROTECTED_SETTINGS
}

For passing in the variable values, I use two files. One titled inputs.auto.tfvars with the following:

# This file contains input values for variables defined in "CLIENT_Variables.tf"
# As this file doesnt contain secrets, it can be committed to source control.
 
subscription = "bc5242b8"
 
location = "eastus2"
 
vmsize = {
  "azclient1" = "Standard_A1_v2"
  "azclientweb" = "Standard_A1_v2"
 }
 
username = "admin"
 
clientcode = "client"
 
serveripaddress = {
  "azclient1" = "10.0.0.211"
  "azclientweb" = "10.0.0.71"
}
 
networkipaddress = {
  "VMNet"                    = "10.0.0.0/23"
  "subnet-lan"               = "10.0.0.0/26"
}
 
resource_group_name = "azclient"
 
dnsservers = ["10.0.0.10", "10.0.0.111"]
dcdnsservers = ["10.0.0.111","10.0.0.10"]
 
#This is the registration URL of Azure Automation
dsc_endpoint = "https://eus2-agentservice-prod-1.azure-automation.net/accounts/guid"
dsc_mode = "ApplyandAutoCorrect"
dsc_configfrequency = "240"
dsc_refreshfrequency = "720"
dsc_nodeconfigname = {
  "dc1"   = "deploymentconfig.domaincontroller"
  "web1"   = "deploymentconfig.webserver"
}

And then another titled secrets.auto.tfvars which does not get uploaded to source control:

password = {
  "azclient1" = "password"
  "azclientweb" = "password"
}
 
dsc_key = "insert key here"

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.