Revisit - Deploying a DC to Azure IaaS with ARM and DSC

Introduction

In an earlier post I provided a walkthrough for the deployment of a Domain Controller to Azure IaaS using an ARM template and DSC. Since that post, I've had several questions due to changes in the way the templates and deployment code do their work.

Today, I'm going to walk through this again with that hope that it clears things up.

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

The other thing you'll need is the xActiveDirectory PowerShell DSC Module. At the time of writing, there was a bug in the latest version (2.13.0.0) that prevented a deployment such as this from working on Windows Server 2016. There's a specific issue fix on GitHub that does work with Windows Server 2016 and is available at

https://github.com/slapointe/xActiveDirectory/tree/Issue73

This should be downloaded and installed to

%ProgramFiles%\WindowsPowerShell\Modules\xActiveDirectory\2.11.0.0

Lastly, it's been my observation that the %PSModulePath% environment variable often contains a duplicate entry for the PowerShell modules residing under %ProgramFiles% -

C:\Program Files\WindowsPowerShell\Modules\xActiveDirectory\2.11.0.0

and

%ProgramFiles%\WindowsPowerShell\Modules\xActiveDirectory\2.11.0.0

This double-up of the path can cause deployment issues. I'd editing the %PSModulePath% environment variable and removing one of these entries if you have both.

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_2

You'll see Deploy-AzureResourceGroup.ps1. This script does all the heavy lifting when Visual Studio is instructed to deploy the solution. In my earlier post, we had to make some edits to this script but with the latest updates, it takes care of everything perfectly.

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_2

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_2

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

ARM07_2

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.

ARM08_2

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 dscDC.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

For this PowerShell data file to be included in the build, right-click on it in the Solution Explorer and select Properties. Configure as follows -

ARM13_2

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" 
        }
   )
}

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": "Microsoft.Powershell.DSC",
              "type": "extensions",
              "location": "[resourceGroup().location]",
              "apiVersion": "2015-06-15",
              "dependsOn": [
                  "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
              ],
              "tags": {
                  "displayName": "dscDC"
              },
              "properties": {
                  "publisher": "Microsoft.Powershell",
                  "type": "DSC",
                  "typeHandlerVersion": "2.9",
                  "autoUpgradeMinorVersion": true,
                  "forceUpdateTag": "[parameters('dscDCUpdateTagVersion')]",
                  "settings": {
                      "configuration": {
                          "url": "[concat(parameters('_artifactsLocation'), '/', variables('dscDCArchiveFolder'), '/', variables('dscDCArchiveFileName'))]",
                          "script": "dscDC.ps1",
                          "function": "Main"
                      },
                      "configurationArguments": {
                          "nodeName": "[variables('vmName')]"
                      }
                  },
                  "protectedSettings": {
                      "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]"
                  }
              }
          }

to -

         {
              "name": "Microsoft.Powershell.DSC",
              "type": "extensions",
              "location": "[resourceGroup().location]",
              "apiVersion": "2015-06-15",
              "dependsOn": [
                  "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
              ],
              "tags": {
                  "displayName": "dscDC"
              },
              "properties": {
                "publisher": "Microsoft.Powershell",
                "type": "DSC",
                "typeHandlerVersion": "2.9",
                "autoUpgradeMinorVersion": true,
                "forceUpdateTag": "[parameters('dscDCUpdateTagVersion')]",
                "settings": {
                  "configuration": {
                    "url": "[concat(parameters('_artifactsLocation'), '/', variables('dscDCArchiveFolder'), '/', variables('dscDCArchiveFileName'))]",
                    "script": "dscDC.ps1",
                    "function": "Main"
                  },
                  "configurationArguments": {
                    "nodeName": "[variables('vmName')]",
                    "domainName": "[parameters('domainName')]",
                    "domainAdminCredentials": {
                      "UserName": "[parameters('adminUserName')]",
                      "Password": "PrivateSettingsRef:Password"
                    }
                  },
                  "configurationData": {
                    "url": "[concat(parameters('_artifactsLocation'), '/DSC/dscDCConfigData.psd1')]"
                  }
                },
                "protectedSettings": {
                  "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]",
                  "items": {
                    "Password": "[parameters('adminPassword')]"
                  }
                }
              }
          }

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_thumb3

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.