Configuring Storage Spaces with Azure Desired State Configuration

AzureAutomationAs promised in my last post, here are the details on configuring Storage Spaces with Azure Desired State Configuration.

But first, some context

The goal is strait forward.  I  deploy multiple Windows Server 2016 VM’s and add them to the domain all with an ARM template.  These VM’s inevitably have multiple data drives just waiting to be provisioned.  Logging into each server to manually configure data drives is just not practical.  I needed a way to pool the data drives and create a  single data disk with minimal interaction.

Enter Storage Spaces

Data drives in Azure have a maximum size of 1TB (or more, depending on the VM and type of storage used) and a set amount of IOPS depending on the underlying storage (SSD or HDD).  Pooling this disks in Azure is a job for Windows Storage Spaces.  PowerShell can be used to script out a process to pool all available Azure data drives into one virtual drive.  This way four, 1TB, 500 IOPS drives of a Standard D1 server can be pooled into one, 4TB, 2000 IOPS drive.

Here is a good starting point to dig into Azure Compute drive size and throughput https://docs.microsoft.com/en-us/azure/virtual-machines/windows/sizes-general

Enter Azure Desired State Configuration

This is where the fun begins.  If it can be scripted by PowerShell it can also be set by Azure Desired State Configuration.  This post assumes you know the basics of DSC.  If you have not worked with Script resources in DSC take a look at this reference.  Below I focus on the Storage Spaces Script Section with the full DSC configuration at the end.

Storage Pool

This section creates the Storage Pool.  It identifies all available “physical” disks in the Storage SubSystem and creates a pool from those disks.  The TestScript Section checks to see if the pool exists before running the SetScript.  Notice the -ErrorAction SilentlyContinue options in the TestScript secion.  More about that here.  The GetScript Section returns the hashtable indicating if the pool is absent or present.

Script StoragePool {
 SetScript = {
     New-StoragePool -FriendlyName StoragePool1 -StorageSubSystemFriendlyName '*storage*' -PhysicalDisks (Get-PhysicalDisk -CanPool $True)
   }
   TestScript = {
     (Get-StoragePool -ErrorAction SilentlyContinue -FriendlyName StoragePool1).OperationalStatus -eq 'OK'
   }
   GetScript = {
     @{Ensure = if ((Get-StoragePool -FriendlyName StoragePool1).OperationalStatus -eq 'OK') {'Present'} Else {'Absent'}}
   }
 }

Virtual Disk

This section gets the available “physical” disks in the pool and creates a virtual disk.  It is recommended to use the same number of columns as physical disks with Storage Spaces in Azure.  It also uses the “simple” resilience setting, relying on Azures underlying storage resilience.  The TestScript and GetScript section is similar to above.  This section has a DependsOn statement to ensure it only runs if the StoragePool script section is configured successfully.

 Script VirtualDisk {
   SetScript = {
     $disks = Get-StoragePool –FriendlyName StoragePool1 -IsPrimordial $False | Get-PhysicalDisk
     $diskNum = $disks.Count
     New-VirtualDisk –StoragePoolFriendlyName StoragePool1 –FriendlyName VirtualDisk1 –ResiliencySettingName simple -NumberOfColumns $diskNum –UseMaximumSize 
   }
   TestScript = {
     (get-virtualdisk -ErrorAction SilentlyContinue -friendlyName VirtualDisk1).operationalSatus -EQ 'OK'
   }
   GetScript = {
     @{Ensure = if ((Get-VirtualDisk -FriendlyName VirtualDisk1).OperationalStatus -eq 'OK') {'Present'} Else {'Absent'}}
   }
   DependsOn = "[Script]StoragePool"
 }

Format the Disk

And finally, we can format the virtual disk.  This set of commands will give it the next available drive letter and format the disk with NTFS and 64KB interleave size.

 Script FormatDisk {
   SetScript = {
     Get-VirtualDisk –FriendlyName VirtualDisk1 | Get-Disk | Initialize-Disk –Passthru | New-Partition –AssignDriveLetter –UseMaximumSize | Format-Volume -NewFileSystemLabel VirtualDisk1 –AllocationUnitSize 64KB -FileSystem NTFS
   }
   TestScript = {
     (get-volume -ErrorAction SilentlyContinue -filesystemlabel VirtualDisk1).filesystem -EQ 'NTFS'
   }
   GetScript = {
     @{Ensure = if ((get-volume -filesystemlabel VirtualDisk1).filesystem -EQ 'NTFS') {'Present'} Else {'Absent'}}
   }
   DependsOn = "[Script]VirtualDisk"
 }

See the Data Disks section of this link for performance best practices https://blogs.msdn.microsoft.com/mast/2014/10/14/configuring-azure-virtual-machines-for-optimal-storage-performance/

Complete DSC configuration:

Here is the finished product.  This DSC script sets the time zone, removes SMB 1 and configures Storage Spaces.

configuration StorageSpace
{
 Param 
 ( 
 #Target nodes to apply the configuration 
 [Parameter(Mandatory = $false)] 
 [ValidateNotNullorEmpty()] 
 [String]$SystemTimeZone="Central Standard Time" 
 ) 
 # Modules to Import
 Import-DscResource –ModuleName PSDesiredStateConfiguration
 Import-DSCResource -ModuleName xTimeZone 
 Node "localhost"
 {
 ###################################
 #Set time zone
 #Add xTimeZone to Azure DSC
 ###################################
 xTimeZone TimeZoneExample { 
 IsSingleInstance = 'Yes'
 TimeZone = $SystemTimeZone 
 } 
 ###################################
 #Feature section
 #Start with disabling SMB 1
 ###################################
 WindowsFeature SMBv1 {
 Name = "FS-SMB1"
 Ensure = "Absent"
 }
 ###################################
 #Script Section
 #Set Storage Spaces
 ###################################
 Script StoragePool {
 SetScript = {
 New-StoragePool -FriendlyName StoragePool1 -StorageSubSystemFriendlyName '*storage*' -PhysicalDisks (Get-PhysicalDisk -CanPool $True)
 }
 TestScript = {
 (Get-StoragePool -ErrorAction SilentlyContinue -FriendlyName StoragePool1).OperationalStatus -eq 'OK'
 }
 GetScript = {
 @{Ensure = if ((Get-StoragePool -FriendlyName StoragePool1).OperationalStatus -eq 'OK') {'Present'} Else {'Absent'}}
 }
 }
 Script VirtualDisk {
 SetScript = {
 $disks = Get-StoragePool –FriendlyName StoragePool1 -IsPrimordial $False | Get-PhysicalDisk
 $diskNum = $disks.Count
 New-VirtualDisk –StoragePoolFriendlyName StoragePool1 –FriendlyName VirtualDisk1 –ResiliencySettingName simple -NumberOfColumns $diskNum –UseMaximumSize 
 }
 TestScript = {
 (get-virtualdisk -ErrorAction SilentlyContinue -friendlyName VirtualDisk1).operationalSatus -EQ 'OK'
 }
 GetScript = {
 @{Ensure = if ((Get-VirtualDisk -FriendlyName VirtualDisk1).OperationalStatus -eq 'OK') {'Present'} Else {'Absent'}}
 }
 DependsOn = "[Script]StoragePool"
 }
 Script FormatDisk {
 SetScript = {
 Get-VirtualDisk –FriendlyName VirtualDisk1 | Get-Disk | Initialize-Disk –Passthru | New-Partition –AssignDriveLetter –UseMaximumSize | Format-Volume -NewFileSystemLabel VirtualDisk1 –AllocationUnitSize 64KB -FileSystem NTFS
 }
 TestScript = {
 (get-volume -ErrorAction SilentlyContinue -filesystemlabel VirtualDisk1).filesystem -EQ 'NTFS'
 }
 GetScript = {
 @{Ensure = if ((get-volume -filesystemlabel VirtualDisk1).filesystem -EQ 'NTFS') {'Present'} Else {'Absent'}}
 }
 DependsOn = "[Script]VirtualDisk"
 }
 }
}

4 thoughts on “Configuring Storage Spaces with Azure Desired State Configuration

  1. Hi

    Does this work with Azure Automation DSC, The reason being you don’t call any storage modules? Can the above just be added to a DSC powershell script and uploaded to Azure Automation DSC Configuration?

  2. Thanks for the scripts. On note though: if I copy the first script right from the page, the hyphen character in front of “CanPool” is coded as E2 80 93 instead of 2d (and visibly three pixels longer)
    That breaks PowerShell.

Leave a Reply

Your email address will not be published.

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