Installing multiple VM extensions on an Azure VM using Terraform

In a recent project, we used Azure Datafactory in a closed network and needed to access resources on-premises. That means that you cannot use the Autoresolve runtime of Datafactory. We thus used our own VM and installed the Self Hosted Integration Runtime software using a VM extension. So far, so good. We needed to install another piece of software on that VM a little later in the project. I started adding another VM extension only to find out that you can only install one VM extension per VM on Azure. We needed to find a way to install multiple dependencies on the VM using a single VM extension in a configurable and manageable way. This blog will describe how we did that using Terraform.

The base of the solution here is a Terraform module that can contain one or more installation scripts. Each installation script itself is a Terraform module. That file structure looks like this:

Terraform Plan in Azure DevOps

Each of that installation script modules is responsible for uploading its needed files to the storage account used by the VM extension. It will then return two outputs: one that contains the installation scripts it uploaded (the VM extensions need to know which scrips or files these are) and the command the VM extension needs to execute to install the software.

output "script_execution" {
  value     = "powershell -ExecutionPolicy Unrestricted -File first.ps1"
  sensitive = true
}

output "files" {
  value = [azurerm_storage_blob.first_script.url]
}

In the main.tf of the VM extension, we will call all the individual installation script modules. The ‘scriptscript_execution’ outputs will be merged in a single file and uploaded to storage like this:

module "first" {
  source = "./first"

  storage_account_name           = var.storage_account_name
  storage_account_container_name = var.storage_account_container_name
}

module "second" {
  source = "./second"

  storage_account_name           = var.storage_account_name
  storage_account_container_name = var.storage_account_container_name
}

resource "azurerm_storage_blob" "combined_script" {
  name                   = "combined_script.ps1"
  storage_account_name   = var.storage_account_name
  storage_container_name = var.storage_account_container_name
  type                   = "Block"
  source_content = join("\n", [
    module.first.script_execution,
    module.second.script_execution
  ])
}

The VM extension will be deployed last. It will use the ‘files’ output of the installation script modules to know which files it needs to download and the ‘combined_script’ blob to know which script it needs to execute.

resource "azurerm_virtual_machine_extension" "vm-extension" {
  name                 = "vm-extension"
  virtual_machine_id   = var.vm_id
  publisher            = "Microsoft.Compute"
  type                 = "CustomScriptExtension"
  type_handler_version = "1.10"

  settings = <<SETTINGS
    {
        "fileUris": ${jsonencode(concat(
        [azurerm_storage_blob.combined_script.url],
        module.first.files,
        module.second.files,
        ))}
    }
  SETTINGS
    protected_settings = <<PROTECTED_SETTINGS
        {
        "commandToExecute": "powershell -ExecutionPolicy Unrestricted -File combined_script.ps1",
        "managedIdentity" : { "objectId": "${var.vm_managedidentity_object_id}" }
        }
    PROTECTED_SETTINGS
}

The complete code can be found on GitHub.