Deploying a DC to Azure IaaS with ARM and DSC

EDIT

This post is obsolete!

I've kept it here to preserve comments and to maintain information that was once relevant. If you are interested in the DC deployment scenario on Azure IaaS with ARM templates and DSC, please refer to the following post -

https://blogs.technet.microsoft.com/markrenoden/2016/11/24/revisit-deploying-a-dc-to-azure-iaas-with-arm-and-dsc/

Introduction

Recently I spent some time building an ARM deployment that included a Domain Controller. In doing so, there were a few obstacles I needed to overcome that I didn't find well explained elsewhere. I'm going to author this post assuming the reader is starting with some Azure IaaS experience but with little-to-no ARM template experience.
There are a number of samples on GitHub demonstrating this outcome. My goal is to explain some of the nuances and to provide some suggestions that make the deployment easier.

Tools

Building ARM templates requires nothing more than a text editor but in my experience, you can't go past Visual Studio with the Azure ADK. For this post, I'll be using one of the pre-canned Azure VMs available to me via my MSDN subscription. It includes everything I need and I don't have to spend time setting it all up -

ARM01

Create an Azure Resource Group Solution

These steps are well discussed elsewhere but I'll include them here for completeness -

  1. Open Visual Studio
  2. Choose New Project
  3. Expand Installed -> Templates -> Visual C# -> Cloud and choose Azure Resource Group in the centre pane
    ARM02
  4. Provide a Name and Solution Name and click OK

Use the Sample VM Template

After creating the new solution, you'll be prompted to select from a series of base templates. You're free to choose a blank template but for the purposes of this blog, I'll select Windows Virtual Machine -

ARM03

Exploring What I Have

Let's start by expanding Scripts and Templates in Solution Explorer -

ARM04

Under Scripts you'll see Deploy-AzureResourceGroup.ps1. This script does all the heavy lifting when Visual Studio is instructed to deploy the solution. I'll spend some time editing it later on.

WindowsVirtualMachine.parameters.json is used to feed per-deployment configuration data into the ARM template.

WindowsVirtualMachine.json is the template that describes the resources deployed to the resource group. Opening this file displays the JSON ready for editing but also opens the JSON Outline in the left-hand pane. Expanding Resources in the JSON Outline gives us an idea of what I get with the sample template -

ARM05

So I'm getting a storage account, a public IP address, a virtual network, a network interface and a virtual machine with an Azure diagnostics extension. All I really need to add is some PowerShell Desired State Configuration that turns the VM into a Domain Controller.

Adding Desired State Configuration

In order to add DSC to the ARM template, right-click the VirtualMachine and select Add New Resource -

ARM06

From the resource list, select PowerShell DSC Extension, provide a name for the extension and select the VM it applies to -

ARM07

After doing so, the DSC Extension appears in the JSON Outline, the JSON itself is added to the ARM template and a new DSC configuration script is added to the solution.

ARM09

Desired State Configuration Script

Edit: Before the following DSC script and PowerShell script edits will work, you need to install the xActiveDirectory DSC module on your tools machine. This module may be downloaded and installed manually from the TechNet gallery or you can install it using Install-Module if using PowerShell 5.0.

 

Now that I have DSC added to the ARM template, I need to set it up to install and configure the Domain Controller role. In this example, I want to deploy the role, the administration tools and configure the new forest root domain along with administrator credentials. To do this, I'll edit the dscDCConfiguration.ps1 script as follows -

 Configuration Main
{

[CmdletBinding()]

Param (
  [string] $NodeName,
 [string] $domainName,
   [System.Management.Automation.PSCredential]$domainAdminCredentials
)

Import-DscResource -ModuleName PSDesiredStateConfiguration, xActiveDirectory

Node $AllNodes.Where{$_.Role -eq "DC"}.Nodename
    {
        LocalConfigurationManager
        {
           ConfigurationMode = 'ApplyAndAutoCorrect'
           RebootNodeIfNeeded = $true
          ActionAfterReboot = 'ContinueConfiguration'
         AllowModuleOverwrite = $true
        }

       WindowsFeature DNS_RSAT
     { 
          Ensure = "Present" 
         Name = "RSAT-DNS-Server"
        }

       WindowsFeature ADDS_Install 
        { 
          Ensure = 'Present' 
         Name = 'AD-Domain-Services' 
        } 

      WindowsFeature RSAT_AD_AdminCenter 
     {
           Ensure = 'Present'
          Name   = 'RSAT-AD-AdminCenter'
      }

       WindowsFeature RSAT_ADDS 
       {
           Ensure = 'Present'
          Name   = 'RSAT-ADDS'
        }

       WindowsFeature RSAT_AD_PowerShell 
      {
           Ensure = 'Present'
          Name   = 'RSAT-AD-PowerShell'
       }

       WindowsFeature RSAT_AD_Tools 
       {
           Ensure = 'Present'
          Name   = 'RSAT-AD-Tools'
        }

       WindowsFeature RSAT_Role_Tools 
     {
           Ensure = 'Present'
          Name   = 'RSAT-Role-Tools'
      }      

     WindowsFeature RSAT_GPMC 
       {
           Ensure = 'Present'
          Name   = 'GPMC'
     } 
      xADDomain CreateForest 
     { 
          DomainName = $domainName            
            DomainAdministratorCredential = $domainAdminCredentials
         SafemodeAdministratorPassword = $domainAdminCredentials
         DatabasePath = "C:\Windows\NTDS"
            LogPath = "C:\Windows\NTDS"
         SysvolPath = "C:\Windows\Sysvol"
            DependsOn = '[WindowsFeature]ADDS_Install'
      }
    }
}

The first thing I've done is add some parameters for the domain name and the domain administrator credentials. This allows me to pass them in from the ARM template -

 Param (
    [string] $NodeName,
 [string] $domainName,
   [System.Management.Automation.PSCredential]$domainAdminCredentials
)

Next I'm importing the PowerShell modules I need -

 Import-DscResource -ModuleName PSDesiredStateConfiguration, xActiveDirectory

I've then applied a filter so that only nodes of role "DC" will be configured as Domain Controllers. This is less important when I'm deploying just one server but in larger deployments where multiple server roles are being deployed, it's useful -

 Node $AllNodes.Where{$_.Role -eq "DC"}.Nodename

The rest of the script installs the required Windows features and finally creates the forest using -

        xADDomain CreateForest 
     { 
          DomainName = $domainName            
            DomainAdministratorCredential = $domainAdminCredentials
         SafemodeAdministratorPassword = $domainAdminCredentials
         DatabasePath = "C:\Windows\NTDS"
            LogPath = "C:\Windows\NTDS"
         SysvolPath = "C:\Windows\Sysvol"
            DependsOn = '[WindowsFeature]ADDS_Install'
      }

Configuration Data for DSC
When credentials are used with DSC, encryption certificates are necessary to protect passwords. Setting this up is beyond what I want to cover here so I'll use a PowerShell data file added to my solution as follows -

ARM10

And then add a PowerShell data file -

ARM11

I add the following contents to the PowerShell data file -

 # Configuration Data for AD  
@{
 AllNodes = @(
       @{
          NodeName="*"
            RetryCount = 20
         RetryIntervalSec = 30
           PSDscAllowPlainTextPassword=$true
           PSDscAllowDomainUser = $true
        },
      @{ 
         Nodename = "localhost" 
         Role = "DC" 
        }
   )
}

Deployment Script Changes to Support DSC
When I instruct Visual Studio to deploy my ARM template, it executes the Deploy-AzureResourceGroup.ps1 script. One function of this script is to package the DSC configuration and upload it to a storage account in your subscription so that the VM can retrieve it during deployment. The code in Deploy-AzureResourceGroup.ps1 does not account for the PowerShell data file I've added,and it assumes that any DSC modules have been imported into the Visual Studio solution. I make changes here by first locating the following code in Deploy-AzureResourceGroup.ps1 -

     # Create DSC configuration archive
    if (Test-Path $DSCSourceFolder) {
        Add-Type -Assembly System.IO.Compression.FileSystem
        $ArchiveFile = Join-Path $ArtifactStagingDirectory "dsc.zip"
        Remove-Item -Path $ArchiveFile -ErrorAction SilentlyContinue
        [System.IO.Compression.ZipFile]::CreateFromDirectory($DSCSourceFolder, $ArchiveFile)
    }

And I replace it with

     # Copy Configuration Data files into staging directory
    Get-ChildItem $DSCSourceFolder -File -Filter '*.psd1' | Copy-Item -Destination $ArtifactStagingDirectory -Force

    # Create DSC configuration archive
  
    if (Test-Path -Path $DSCSourceFolder)
   {
       Get-ChildItem -Path $DSCSourceFolder -Filter *.ps1 | ForEach-Object {

           $archiveName = $_.BaseName + '.ps1.zip'
         $archivePath = Join-Path -Path $ArtifactStagingDirectory -ChildPath $archiveName
            
            # Create the .ps1.zip file DSC Archive
          Publish-AzureRmVMDscConfiguration -ConfigurationPath $_.FullName `
              -OutputArchivePath $archivePath `
               -Force `
                -Verbose
        }
   }

The script now copies all PowerShell data files into the staging directory and creates a DSC archive for each DSC script in the DSC folder. The beauty of using Publish-AzureRmVMDscConfiguration is that any PowerShell modules referenced by the Desired State Configuration script (using the Import-DscResource cmdlet) are automatically included in the package, provided they're installed on the tools machine (critical).

The packages and PowerShell data files are uploaded to an Azure storage account during deployment.

ARM Template Changes to Support DSC

The next step is to add some parameters for the domain name and admin credentials. The parameters file - WindowsVirtualMachine.parameters.json already contains -

 {
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "value": null
    },
    "dnsNameForPublicIP": {
      "value": null
    }
  }
}

I update it to contain -

 {
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "value": "mark"
    },
    "adminPassword": {
      "value": "P@ssw0rd123!"
    },
    "domainName": {
      "value": "contoso.com"
    },
    "dnsNameForPublicIP": {
      "value": "blogdc01"
    },
    "windowsOSVersion": {
      "value": "2012-R2-Datacenter"
    }
  }
}

adminPassword and windowsOSVersion are already a defined parameters in the WindowsVirtualMachine.json template file. All I need to do is add domainName to the parameters section using -

 {
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Username for the Virtual Machine."
      }
    },
    "adminPassword": {
      "type": "securestring",
      "metadata": {
        "description": "Password for the Virtual Machine."
      }
    },
    "domainName": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Domain Name for the Forest."
      }
    },
    "dnsNameForPublicIP": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Globally unique DNS Name for the Public IP used to access the Virtual Machine."
      }
    },
    "windowsOSVersion": {
      "type": "string",
      "defaultValue": "2012-R2-Datacenter",
      "allowedValues": [
        "2008-R2-SP1",
        "2012-Datacenter",
        "2012-R2-Datacenter"
      ],
      "metadata": {
        "description": "The Windows version for the VM. This will pick a fully patched image of this given Windows version. Allowed values: 2008-R2-SP1, 2012-Datacenter, 2012-R2-Datacenter."
      }
    },

Lastly, I'll modify the DSC extension in the WindowsVirtualMachine.json template file from -

         {
          "name": "dscDC",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-06-15",
          "dependsOn": [
            "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
          ],
          "tags": {
            "displayName": "dscDC"
          },
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.9",
            "autoUpgradeMinorVersion": true,
            "settings": {
              "modulesUrl": "[concat(parameters('_artifactsLocation'), '/', 'dsc.zip')]",
              "sasToken": "[parameters('_artifactsLocationSasToken')]",
              "configurationFunction": "[variables('dscDCConfigurationFunction')]",
              "properties": {
                "nodeName": "[variables('vmName')]"
              }
            },
            "protectedSettings": { }
          }
        }

to -

         {
          "name": "dscDC",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-06-15",
          "dependsOn": [
            "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
          ],
          "tags": {
            "displayName": "dscDC"
          },
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.9",
            "autoUpgradeMinorVersion": true,
            "settings": {
              "modulesUrl": "[concat(parameters('_artifactsLocation'), '/', 'dscDCConfiguration.ps1.zip')]",
              "sasToken": "[parameters('_artifactsLocationSasToken')]",
              "configurationFunction": "[variables('dscDCConfigurationFunction')]",
              "properties": {
                "nodeName": "[variables('vmName')]",
                "domainName": "[parameters('domainName')]",
                "domainAdminCredentials": {
                  "UserName": "[parameters('adminUserName')]",
                  "Password": "PrivateSettingsRef:Password"
                }
              }
            },
            "protectedSettings": {
              "items": {
                "Password": "[parameters('adminPassword')]"
              },
              "DataBlobUri": "[concat(parameters('_artifactsLocation'), '/dscDCConfigData.psd1')]"
            }
          }
        }

Here I've modified the modulesUrl to match the name of the DSC archive that will be used for the DC, I've added domainName and domainAdminCredentials properties that will be passed to the DSC script and I've added the adminPassword and the DataBlobUri to protectedSettings. The DataBlobUri is the location for the PowerShell data file used for DSC config data.

Deployment

At this stage I'm ready to deploy my DC to Azure. All I need to do is right-click the solution name, select Deploy and then New Deployment.

ARM12

Following the wizard kicks off the deployment and after a short wait, the deployment is complete.

Conclusion

My hope is that this post clears up a few questions around Azure Resource Manager (ARM) template deployments and integration with DSC. This is only the start of what's possible with ARM template deployments that permit multi-VM builds with any number of customisations.