Techdecline's Blog

The Case of the unknown Azure Disk

Problem Statement

There is often a gap between Infrastructure as Code (the VM Deployment) and Configuration as Code where certain information must be shared across both domains.

Recently, I wanted to deploy a Windows virtual machine on Azure with a couple of data disks attached and configure the machine using Desired State Configuration. I failed miserably for quiet some time as I never knew for sure how I could identify the disks in the DSC configuration as using the Disk Number leads to issues as described within the docs of StorageDSC.

Using the location property also failed sometimes based on Azure Managed Disk SKU selection and Virtual Machine SKU as shown below:

Disk DataDisk
{
	DiskId = 'Integrated : Bus 0 : Device 63667 : Function 30747 : Adapter 3 : Port 0 : Target 0 : LUN 10'
	DriveLetter = 'E'
	AllocationUnitSize = 65536
	DiskIdType = 'Location'
	FSFormat = 'NTFS'
	FSLabel = 'DATA'
	PartitionStyle = 'GPT'
	AllowDestructive = $false
}

Disk LogDisk
{
	DiskId = 'Integrated : Bus 0 : Device 63667 : Function 30747 : Adapter 3 : Port 0 : Target 0 : LUN 11'
	DriveLetter = 'F'
	AllocationUnitSize = 65536
	DiskIdType = 'Location'
	FSFormat = 'NTFS'
	FSLabel = 'LOG'
	PartitionStyle = 'GPT'
	AllowDestructive = $false
}

Terraform-based Solution

As I stepped back from using DSC for Disk Formatting, I found a blog from Jack Ropper on the web that proposed to use a CustomScriptExtension resource for formatting the drives.

This is elegant, but I needed to extend on it to bundle it within the Virtual Machine Module we're using as we allow multiple data disks where some of them might be formatted and some not.

Variable Setup

Within the variables.tf, there is a variable for additional data disks.

variable "disk_config" {
type = list(object(
	{
		disk_number = number
		disk_storage_account_type = string
		disk_size_gb = string
		data_disk_cache = string
		disk_label = optional(string)
		disk_file_system = optional(string)
		disk_drive_letter = optional(string)
	}
	))
}

Disk Creation

In addition to that, the disks are flattened to be used in for_each constructs for disk creation and attachment as shown below.

locals {
    disk_config = flatten([
    for disk in var.disk_config : {
      disk_number               = disk.disk_number
      disk_storage_account_type = disk.disk_storage_account_type
      disk_size_gb              = disk.disk_size_gb
      data_disk_cache           = disk.data_disk_cache
      disk_label                = disk.disk_label
      disk_file_system          = disk.disk_file_system
      disk_drive_letter         = disk.disk_drive_letter
    }
  ])
}

resource "azurerm_managed_disk" "disk" {
  for_each = {
    for disk_config in local.disk_config : "${disk_config.disk_number}" => disk_config
  }
  name                   = "disk-${var.name}-${each.value.disk_number}"
  location               = var.location
  resource_group_name    = var.resource_group_name
  storage_account_type   = each.value.disk_storage_account_type
  create_option          = "Empty"
  disk_size_gb           = each.value.disk_size_gb
  tags                   = var.tags
  lifecycle {
    ignore_changes = [
      create_option,
      source_resource_id,
    ]
  }
}

resource "azurerm_virtual_machine_data_disk_attachment" "vm-disk" {
  for_each = {
    for disk_config in local.disk_config : "${disk_config.disk_number}" => disk_config
  }
  managed_disk_id           = azurerm_managed_disk.disk[each.value.disk_number].id
  virtual_machine_id        = var.vm_os_type == "Windows" ? azurerm_windows_virtual_machine.vm[0].id : azurerm_linux_virtual_machine.vm.id
  lun                       = 10 + each.value.disk_number
  caching                   = each.value.data_disk_cache
  write_accelerator_enabled = var.write_accelerator_enabled
}

Formatting the Drive(s)

PowerShell Script for Templating

At scripts/FormatDisk.ps1, we generate a file containing the following snippet:

Get-Disk | Where-Object {($_.Location -split ' ' )[-1] -eq ${lun_id}} | Initialize-Disk -PartitionStyle ${format_style} -PassThru | New-Partition -UseMaximumSize -DriveLetter ${drive_letter} | Format-Volume -FileSystem NTFS -NewFileSystemLabel ${disk_label} -Confirm:$false

This template gets rendered for every disk that has a disk_drive_letter assigned.

data "template_file" "diskpart_windows" {
  for_each = {
    for disk_config in local.disk_config : "${disk_config.disk_number}" => disk_config
    if disk_config.disk_drive_letter != null
  }
  template = "${file("${path.module}/scripts/FormatDisk.ps1")}"
  vars = {
    lun_id = 10 + each.value.disk_number
    format_style = each.value.disk_file_system
    drive_letter = each.value.disk_drive_letter
    disk_label = each.value.disk_label
  }
}

Another locals block is used to flatten all templates:

locals {
	rendered_diskpart_list = flatten([
	    for template in data.template_file.diskpart_windows : {
	        rendered = template.rendered
	    }])
}

NOTE: Azure only allows one CustomScriptExtension resource. Therefor, the templates need to be concatenated into a single file.

Creating the Script Resource

As a last step, all rendered template files will be base64-encoded and wrapped within a CustomScriptResource

resource "azurerm_virtual_machine_extension" "disk_init" {
  count                = try(data.template_file.diskpart_windows, null) != null ? 1 : 0
  name                 = "vm-disk-init-ext"
  virtual_machine_id   = azurerm_windows_virtual_machine.vm.id
  publisher                  = "Microsoft.Compute"
  type                       = "CustomScriptExtension"
  type_handler_version       = "1.9"
  auto_upgrade_minor_version = "true"

  settings = <<SETTINGS
    {
          "commandToExecute": "powershell -command \"[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64encode(join("\n",local.rendered_diskpart_list.*.rendered))}')) | Out-File -filepath FormatDisk.ps1\" && powershell -ExecutionPolicy Unrestricted -File FormatDisk.ps1"
    }
SETTINGS

  tags = var.tags
  depends_on = [
    azurerm_virtual_machine_data_disk_attachment.vm-disk,
    azurerm_virtual_machine_extension.vm-domjoin
  ]
}

Wrapping things up

It's getting messy sometimes when integrating IaC and CaC solutions like Terraform, Ansible or DSC as there is always need for interaction or data exchange.

Generally speaking, most actions that happen from within a given resource should be managed using a CaC tool, but there are exceptions to the rule as shown in this post.

#azure #powershell #terraform