As 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" } } }
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?
Yes, tested and works with Azure DSC. Its a PowerShell Script so it only needs the PowerShell Script module to run.
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.
Thank you! I updated the page. This is why I now use Git to share code.