Terraform – Variables in resource names

This is a post about resource naming in Terraform. As I was working on my first production use of Terraform to deploy resources to Azure, my goal was to parameterize the resource so that in the future I could easily re-use the .TF file with a simple replacement of my .TFVARS file.

I struggled for a while because I was trying to do something like this:

resource "azurerm_virtual_network" "AZ${var.clientcode}Net" {
  name                = "AZ${var.clientcode}"
  address_space       = ["${lookup(var.networkipaddress, "AZ${var.clientcode}")}"]
  location            = "${var.location}"
  resource_group_name = "${azurerm_resource_group.Default.name}"
  dns_servers         = "${var.dnsservers}"
}

Effectively, I wanted my resource to result in something like “AZABCNet” both in the name property and the resource identifier as well.
Then I would try and reference that in a different resource attribute, for a subnet like this:

virtual_network_name = "${azurerm_virtual_network.AZ${var.clientcode}Net.name}"

However, this gave me errors when trying to validate:

Error: Error loading C:\terraform\ABC\ABC_Network.tf: Error reading config for azurerm_subnet[${var.location}]: parse error at 1:28: expected expression but found invalid sequence "$"

 

My assumption of how the resource identifier worked was incorrect thinking; resource names should be local and static, as described here on StackOverflow.

This means that I can give my resource an identifier as “AZClientNet”, since that is used only within Terraform while the Name attribute is what will become the deployed name in Azure.

 

This leaves a little bit of a gap where I might want a resource declared with multiples of something, with attributes defined in a map variable, without having to declare multiple resource blocks.

It appears this functionality is coming in Terraform in the form of a “for each” function, according to this Github discussion. Not having used the proposed syntax in a test environment, I don’t fully have my head around it.

ARM Template to Azure Automation DSC

Either my google skills are deteriorating, or getting an ARM Template virtual machine connected to an Azure Automation DSC node configuration through the DSC extension is both not obvious and poorly documented.

I came across two different ways of achieving my goal, which was to run a single PowerShell command consisting of “New-AzureRmResourceGroupDeployment” with a template file and parameter file, and have it both deploy the resources, install the DSC extension, assign it to a node configuration, and bring it into compliance.

The first is in the official documentation, referenced here as Automation DSC Onboarding. Within that article, it provides this GitHub quick start template as the reference. With this template, you specify your external Automation URL directly, along with the Key to authorize the connection, and then a method to grab the “UpdateLCMforAAPull” configuration module needed, which in this case comes from the raw github repository, although I’ve seen other examples of storing it in a private Storage Account blob.

Interestingly, as of the time I’m writing this post, that GitHub page has been updated to reflect it is no longer necessary – this was not the case as I worked through this problem.

During this investigation I came across this feedback article relating to connecting an ARM deployment to Azure Automation DSC. This linked to a different GitHub quick start template which is similar but has syntax to pull the Automation URL and Key from a referenced lookup rather than statically set, and removes the need for definition of the extra configuration module. That feedback article also gives some insight into why the documentation and code examples floating around are confusing – being in a state of transition has that effect.

Two very important lessons I learned once I hit on an accurate template and began testing:

First: Using the second (more recent) example, your Automation account must be in the same resource group that you’re deploying other resources to, because within the resource template for the Automation Account, there isn’t a value for Resource Group. Perhaps this is my inexperience with ARM but looking at the API reference gives no indication this is possible, and when I had a mismatch it produced unavoidable errors.

Secondly: Watch out for Node Configuration case mismatch – as in upper vs lower case. My node configuration was compiled using configuration data, so it was named in the format <ConfigName>.<virtualMachineName>, with <virtualMachineName> being all lowercase. However when I used parameter values in my JSON file to reference that node configuration, it was done with mixed case.

The result of this was that the virtual machine would get deployed successfully, as would the DSC extension. It would begin applying the DSC configuration according to the logs, and running “Get-AzureRmAutomationDSCNode” listed my nodes with check-in timestamps and a status of “Compliant”. However, these nodes would NOT appear in the Portal under “DSC Nodes”. I suspect this is related to the filtering happening at the top of the page, where it pre-selects all node configurations (which are lower case) and thus the filter doesn’t match.

Once I corrected this in my template and re-ran the deployment, they appeared properly in the GUI.

 

For anyone else looking to achieve this, here’s my config (this is a single VM and VNIC attaching to a pre-existing subnet in Azure):

Parameter.JSON file:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "ClientCode": {
            "value": "ABCD"
        },
        "TimeZone": {
            "value": "Central Standard Time"
        },
        "ProdWeb": {
            "value": {
                "StaticIP": "10.7.12.212",
                "VMSize": "Standard_A1_V2",
                "NameDigit": "2"
            }
        }
        "ProdWebPassword": {
            "reference": {
                "keyVault": {
                    "id": "/subscriptions/insertsubscriptionID/resourceGroups/insertResourceGroupName/providers/Microsoft.KeyVault/vaults/VaultName"
                },
                "secretName": "ProdWeb-Admin"
            }
        }
    }
}

Deployment.JSON file:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "ClientCode": {
            "type": "string",
            "metadata": {
                "description": "Client Code to be used for Deployment (uppercase)"
            }
        },
        "TimeZone": {
            "type": "string",
            "metadata": {
                "description": "Define the time zone to apply to virtual machines"
            }
        },
        "windowsOSVersion": {
            "defaultValue": "2012-R2-Datacenter",
            "allowedValues": [
                "2012-R2-Datacenter",
                "2016-Datacenter"
            ],
            "type": "string",
            "metadata": {
                "description": "The Windows version for the VM. "
            }
        },
        "ProdWeb": {
            "type": "object",
            "metadata": {
                "description": "Object parameter containing information about PextWeb2 virtual machine"
            }
        }
        "ProdWebPassword": {
            "type": "securestring"
        }
    },
    "variables": {
        "ProdWebName": "[concat('AZ',parameters('ClientCode'),'ProdWeb', parameters('ProdWeb').NameDigit)]",
        "virtualNetwork": "[resourceID('Microsoft.Network/virtualNetworks', concat('AZ',toupper(parameters('ClientCode'))))]",
        "Subnet-Prod-Web-Servers": "[concat(variables('virtualNetwork'),'/Subnets/', concat('AZ',toupper(parameters('ClientCode')),'-Prod-Web-Servers'))]",
        "AdminUser": "ouradmin",
        "storageAccountType": "Standard_LRS",
        "automationAccountName": "AutomationDSC",
        "automationAccountLocation": "EastUS2",
        "ConfigurationName": "ProdWebConfig",
        "configurationModeFrequencyMins": "240",
        "refreshFrequencyMins": "720"
    },
    "resources": [
        {
            "comments": "Automation account for DSC",
            "apiVersion": "2015-10-31",
            "location": "[variables('automationAccountLocation')]",
            "name": "[variables('automationAccountName')]",
            "type": "Microsoft.Automation/automationAccounts",
            "properties": {
                "sku": {
                    "name": "Basic"
                }
            }
        },
        {
            "comments": "VNIC for ProdWeb",
            "apiVersion": "2017-06-01",
            "type": "Microsoft.Network/networkInterfaces",
            "name": "[concat(variables('ProdWebName'),'nic1')]",
            "location": "[resourceGroup().location]",
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAllocationMethod": "Static",
                            "privateIPAddress": "[parameters('ProdWeb').StaticIP]",
                            "subnet": {
                                "id": "[variables('Subnet-Prod-Web-Servers')]"
                            }
                        }
                    }
                ]
            }
        },
        {
            "comments": "Disk E for ProdWeb",
            "type": "Microsoft.Compute/disks",
            "name": "[concat(variables('ProdWebName'),'_E')]",
            "apiVersion": "2017-03-30",
            "location": "[resourceGroup().location]",
            "properties": {
                "creationData": {
                    "createOption": "Empty"
                },
                "diskSizeGB": 64
            },
            "sku": {
                "name": "[variables('storageAccountType')]"
            }
        },
        {
            "comments": "VM for ProdWeb",
            "type": "Microsoft.Compute/virtualMachines",
            "name": "[variables('ProdWebName')]",
            "apiVersion": "2017-03-30",
            "location": "[resourceGroup().location]",
            "properties": {
                "hardwareProfile": {
                    "vmSize": "[parameters('ProdWeb').VMSize]"
                },
                "osProfile": {
                    "computerName": "[variables('ProdWebName')]",
                    "adminUsername": "[variables('AdminUser')]",
                    "adminPassword": "[parameters('ProdWebPassword')]",
                    "windowsConfiguration": {
                        "enableAutomaticUpdates": false,
                        "provisionVMAgent": true,
                        "timeZone": "[parameters('TimeZone')]"
                    }
                },
                "storageProfile": {
                    "imageReference": {
                        "publisher": "MicrosoftWindowsServer",
                        "offer": "WindowsServer",
                        "sku": "[parameters('windowsOSVersion')]",
                        "version": "latest"
                    },
                    "osDisk": {
                        "name": "[concat(variables('ProdWebName'),'_C')]",
                        "createOption": "FromImage",
                        "diskSizeGB": 128,
                        "caching": "ReadWrite",
                        "osType": "Windows",
                        "managedDisk": {
                            "storageAccountType": "[variables('storageAccountType')]"
                        }
                    },
                    "dataDisks": [
                        {
                            "lun": 0,
                            "createOption": "Attach",
                            "caching": "ReadWrite",
                            "managedDisk": {
                                "id": "[resourceId('Microsoft.Compute/disks', concat(variables('ProdWebName'),'_E'))]"
                            }
                        }
                    ]
                },
                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('ProdWebName'),'nic1'))]"
                        }
                    ]
                },
                "diagnosticsProfile": {
                    "bootDiagnostics": {
                        "enabled": false
                    }
                }
            },
            "dependsOn": [
                "[concat('Microsoft.Network/networkInterfaces/', concat(variables('ProdWebName'),'nic1'))]",
                "[concat('Microsoft.Compute/disks/', concat(variables('ProdWebName'),'_E'))]"
            ]
        },
        {
            "comments": "DSC extension config for ProdWeb",
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat(variables('ProdWebName'),'/Microsoft.Powershell.DSC')]",
            "apiVersion": "2017-03-30",
            "location": "[resourceGroup().location]",
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachines/', variables('ProdWebName'))]"
            ],
            "properties": {
                "publisher": "Microsoft.Powershell",
                "type": "DSC",
                "typeHandlerVersion": "2.75",
                "autoUpgradeMinorVersion": true,
                "protectedSettings": {
                    "Items": {
                        "registrationKeyPrivate": "[listKeys(resourceId('Microsoft.Automation/automationAccounts/', variables('automationAccountName')), '2015-01-01-preview').Keys[0].value]"
                    }
                },
                "settings": {
                    "Properties": [
                        {
                            "Name": "RegistrationKey",
                            "Value": {
                                "UserName": "PLACEHOLDER_DONOTUSE",
                                "Password": "PrivateSettingsRef:registrationKeyPrivate"
                            },
                            "TypeName": "System.Management.Automation.PSCredential"
                        },
                        {
                            "Name": "RegistrationUrl",
                            "Value": "[reference(concat('Microsoft.Automation/automationAccounts/', variables('automationAccountName'))).registrationUrl]",
                            "TypeName": "System.String"
                        },
                        {
                            "Name": "NodeConfigurationName",
                            "Value": "[concat(variables('ConfigurationName'),'.',tolower(variables('ProdWebName')))]",
                            "TypeName": "System.String"
                        },
                        {
                            "Name": "ConfigurationMode",
                            "Value": "ApplyandAutoCorrect",
                            "TypeName": "System.String"
                        },
                        {
                            "Name": "RebootNodeIfNeeded",
                            "Value": true,
                            "TypeName": "System.Boolean"
                        },
                        {
                            "Name": "ActionAfterReboot",
                            "Value": "ContinueConfiguration",
                            "TypeName": "System.String"
                        },
                        {
                            "Name": "ConfigurationModeFrequencyMins",
                            "Value": "[variables('configurationModeFrequencyMins')]",
                            "TypeName": "System.Int32"
                        },
                        {
                            "Name": "RefreshFrequencyMins",
                            "Value": "[variables('refreshFrequencyMins')]",
                            "TypeName": "System.Int32"
                        }
                    ]
                }
            }
        }
    ],
    "outputs": {}
}

 

 

Terraform – Simple virtual machine

As mentioned on my Terraform – First Experience post, I began with a very simple set of resources to stand up a single virtual machine.

The following can be placed into a .TF file, and used right away with “terraform plan” and “terraform apply”. This would be much more useful if every resource wasn’t named “test”, but for the purposes of walking through each resource, understanding the structure of variables and dependency syntax its a good beginning.

provider "azurerm" {
  subscription_id = "f745d13d"
  client_id       = "7b011238"
  client_secret   = "fhSZ6YN"
  tenant_id       = "461b7ed5"
}

variable "location" { default = "EastUS" }
variable "username" { default = "adminuser" }
variable "password" { default = "password" }

resource "azurerm_resource_group" "test" {
  name     = "HelloWorld"
  location = "${var.location}"
}

resource "azurerm_virtual_network" "test" {
  name                = "test"
  address_space       = ["10.0.0.0/16"]
  location            = "${var.location}"
  resource_group_name = "${azurerm_resource_group.test.name}"
}
 
resource "azurerm_subnet" "test" {
  name                 = "test"
  resource_group_name  = "${azurerm_resource_group.test.name}"
  virtual_network_name = "${azurerm_virtual_network.test.name}"
  address_prefix       = "10.0.2.0/24"
}
 
resource "azurerm_network_interface" "test" {
  name                = "test"
  location            = "${var.location}"
  resource_group_name = "${azurerm_resource_group.test.name}"
  ip_configuration {
    name                          = "testconfiguration1"
    subnet_id                     = "${azurerm_subnet.test.id}"
    private_ip_address_allocation = "dynamic"
  }
}
 
resource "azurerm_storage_account" "test" {
  name                = "helloworlduniquenumbers"
  resource_group_name = "${azurerm_resource_group.test.name}"
  location            = "${var.location}"
  account_replication_type  = "LRS"
  account_tier        = "Standard"
}
 
resource "azurerm_storage_container" "test" {
  name                  = "helloworld"
  resource_group_name   = "${azurerm_resource_group.test.name}"
  storage_account_name  = "${azurerm_storage_account.test.name}"
  container_access_type = "private"
}
 
resource "azurerm_virtual_machine" "test" {
  name                  = "helloworld"
  location              = "${var.location}"
  resource_group_name   = "${azurerm_resource_group.test.name}"
  network_interface_ids = ["${azurerm_network_interface.test.id}"]
  vm_size               = "Standard_A1"
  
  storage_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2016-Datacenter"
    version   = "latest"
  }
 
  storage_os_disk {
    name          = "myosdisk1"
    vhd_uri       = "${azurerm_storage_account.test.primary_blob_endpoint}${azurerm_storage_container.test.name}/myosdisk1.vhd"
    caching       = "ReadWrite"
    create_option = "FromImage"
  }
 
  os_profile {
    computer_name  = "helloworld"
    admin_username = "${var.username}"
    admin_password = "${var.password}"
  }

  os_profile_windows_config {
    provision_vm_agent = true
    enable_automatic_upgrades = true
  }
 
}

ARM Template – reference existing resources

Despite being a rather organized person in my professional life, I begin learning new topics and technologies in quite the opposite manner.

As I begin to dive into Azure automation, today I wanted to understand the proper syntax to deploy small resources with an ARM template while referencing existing resources, rather than what is declared and built within the JSON file itself.

This Azure Quickstart template was very valuable as I spent some time exploring this.

The basic idea:

  • Virtual Network and Subnet already exist
  • Add Network Interface through ARM template

Once I wrapped my head around the proper way to input the names of existing resource group, virtual network, and subnet, it all kind of clicked for me.

Resource Group is defined during the PowerShell command that calls the JSON file (I’ll show this at the bottom of this post). Parameters are built to accept simple text strings of the virtual network name and subnet name that I’m targeting. Then I used these parameters to build a variable for the subnet, and used that variable in the resource declaration for the network interface.

Here’s the JSON:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "virtualNetworkName": {
            "type": "string",
            "metadata": {
                "description": "Type existing virtual network name"
            }
        }
        ,"subnetName": {
            "type": "string",
            "metadata": {
                "description": "Type existing Subnet name"
            }
        }
    },
    "variables": {
        ,"subnetRef": "[resourceID('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
    },
    "resources": [
        {
            "apiVersion": "2015-06-15",
            "type": "Microsoft.Network/networkInterfaces",
            "name": "WindowsVM1-NetworkInterface",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "WindowsVM1 Network Interface"
            },
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAllocationMethod": "Dynamic",
                            "subnet": {
                                "id": "[variables('subnetRef')]"
                            }
                        }
                    }
                ]
            }
        }

    ],
    "outputs": {}
}
To deploy this, I make a connection to Azure PowerShell, and ran:
New-AzureRmResourceGroupDeployment -ResourceGroupName "Group1" -TemplateFile "C:\Azure\Templates\NewSubnet.json"
This prompts me for my two parameters, which I could set defaults for in the JSON or provide a parameter file and use that in the deployment command as well.
Next up – deploy a full VM with managed disks attaching to existing resources, and then working into Desired State Config.

Terraform – Import Azure Resources

One of the first uses I’ll have for Terraform in my work will be adding resources to an existing environment – an environment for which Terraform has no state information. This means when I’m declaring the new VMs and want to tie it to a Resource Group, Terraform won’t have a matching resource for that.

Today I’ve been playing around with Terraform import in my sandbox to become familiar with the process. In my sandbox I have an existing Resource Group, Virtual Network, and Subnet. I intended to add a simple network interface, tied to an already-existing subnet.

To begin, I declared my existing resources in my .TF file as I would want them to exist (technically matching how they exist right now):

resource "azurerm_resource_group" "Client_1" {  
 name = "Client_1"
 location = "${var.location}"
 }

resource "azurerm_virtual_network" "Client1Network" {
 name = "Client1Network"
 address_space = ["10.1.0.0/16"]
 location = "${var.location}"
 resource_group_name = "${azurerm_resource_group.Client_1.name}"
 }

resource "azurerm_subnet" "Web" {
 name = "Web"
 resource_group_name = "${azurerm_resource_group.Client_1.name}"
 virtual_network_name = "${azurerm_virtual_network.Client1Network.name}"
 address_prefix = "10.1.10.0/24"
 }

Then for each of them I gathered the Resource ID in Azure. For the resource group and virtual network this was simple enough; find the Properties pane and copy the string that was there:

For the Subnet, there wasn’t an easy GUI reference that I could find, so I turned to Azure PowerShell, which output the ID I needed:

$vmnet = get-azurermVirtualnetwork | where {$_.Name -eq "Client1Network" }

get-azurermvirtualnetworksubnetconfig -virtualnetwork $vmnet

Then I used the Terraform “import” command along with the resource declaration and name in my file, and the resource ID from Azure:

terraform import azurerm_resource_group.Client_1 /subscriptions/f745d13d/resourceGroups/Client_1/providers/Microsoft.Network/virtualNetworks/Client1Network

I repeated the process for the resource group, virtual network, and subnet.

Then I added the resource declartaion in my .TF file for the network interface I wanted to add:

resource "azurerm_network_interface" "testNIC" {
 name = "testNIC"
 location = "${var.location}"
 resource_group_name = "${azurerm_resource_group.Client_1.name}"
 ip_configuration {
 name = "testconfiguration1"
 subnet_id = "${azurerm_subnet.Web.id}"
 private_ip_address_allocation = "dynamic"
 }
}

Then I performed a “terraform plan”, which showed me the resource it detected needing to be created:

 

Once I completed the “terraform apply”, the resource was created and visible within my Azure portal.