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.

Comments

  • Anonymous
    November 27, 2016
    I keep getting errors when changing properties on Powershell Data Filemismatched PageRule with the itemtype
    • Anonymous
      November 27, 2016
      Hi MontereyTry changing "Copy to Output Directory" to "Copy always" first, hit "Apply" and then change "Build Action" to "Content" and hit "Apply". After that, I think you should be OK.CheersMark
  • Anonymous
    November 27, 2016
    Commenting here too -Could you point me towards something that explains how passing credentials into DSC works? I don’t think the fist DSC config I have is actually using any credentials at all when standing up that first domain controller; I think it’s using the first user’s (local admin) account. I am working on an ARM template with two domain controllers, and adding the second isn’t working.The reason I think this is because now that I am working on adding a second domain controller (which checks for a domain and so would need domain credentials), I can’t get the DSC config to work. So I guess I am not sure how to check for what is actually being passed into this DSC config and what credentials it is actually using to check for the domain, but it’s definitely wrong and I have followed a ton of other people’s examples.To see if maybe there was something wrong with the master xActiveDirectory module, I grabbed the -dev branch one and used that and I do get another error letting me know for sure that either the credentials are wrong or the domain is wrong (it isn’t).
    • Anonymous
      November 27, 2016
      I haven't tried that myself. Remember that DSC is a capability separate to ARM templates and you're trying to put them together to achieve the outcome. I think this blog post is a good reference for DSC and credentials on its own -https://blogs.msdn.microsoft.com/powershell/2014/01/31/want-to-secure-credentials-in-windows-powershell-desired-state-configuration/You could also take a look at the 2 DC ARM template example on GitHub - https://github.com/Azure/azure-quickstart-templates/tree/master/active-directory-new-domain-ha-2-dcNote that my configuration data file tells DSC to ignore unencrypted credentials. If you're going to protect credentials, you're going to need to include certificates in your workflow ...
  • Anonymous
    November 30, 2016
    Can I deploy this Using Azure Automation Account. I like to distribute this in DSC configuration file in Azure automation.
    • Anonymous
      December 04, 2016
      My understanding is yes. I haven't put any time into this myself but I know a colleague has successfully applied DSC to Azure VMs via Azure Automation ... which of course provides you with ongoing configuration drift prevention.
  • Anonymous
    December 03, 2016
    Hi,Sorry do you have some clearer steps on what you need to do to get this working on Server 2016?
    • Anonymous
      December 04, 2016
      I'm not sure what you're missing. For 2016, go to the GitHub link I provided, download everything and dump it in the %ProgramFiles%\WindowsPowerShell\Modules\xActiveDirectory\2.11.0.0 folder.I don't think I checked when I wrote this. You probably have to add the 2016 OS as an allowed value in the parameters section of your ARM template.
      • Anonymous
        December 23, 2016
        The comment has been removed
      • Anonymous
        December 23, 2016
        The comment has been removed
      • Anonymous
        December 26, 2016
        Sorry - figured it out...Might be an oversight but I didn't realise my machine that was running VS had to have the xActiveDirectory on...Install-Module -Name xActiveDirectory -RequiredVersion 2.15.0.0Is it this directory on my laptop I need to update with that issue 73 for the 2016 server?
        • Anonymous
          December 27, 2016
          When I wrote this post, 2.12 was the current version of xActiveDirectory. Issue 73 was not included in 2.12. From your comment, it seems 2.15 is the current version which may work successfully on Server 2016. As I said, all of this is a moving target. You can try 2.15. If that doesn't work, use the fix as I've described.
        • Anonymous
          February 13, 2017
          The comment has been removed
  • Anonymous
    December 07, 2016
    Hi Mark,Here is a slightly different way to create a DC using ARM and DSC. I created a second hard disk on the DC VM (data disk with no caching) because that the SYSVOL and ADDS database have to be on non-cached non OS disk. I am also passing parameters as secure strings so the passwords are requested at deployment time instead of being entered in the code. The steps are posted here: http://ghassanhariz7.azurewebsites.net/2016/12/07/building-adds-dc-azure-iaas-using-arm-dsc/. The one issue I had was with the .psd1 file not being read at deployment time. I worked around that by putting the .psd1 file on Github. Any comments or feedback are appreciated.
    • Anonymous
      December 07, 2016
      Great! This post was really just serving as an example to get folks up and running. Addressing things like additional disks and passing secured parameters are certainly valid and useful. Thanks for adding value!
    • Anonymous
      December 28, 2016
      Hi GhassanHariz,In your article, which is great, the problem you are getting where it can't find the resource - you're putting the files on GITHUB to workaround the problem, but if you create an azure storage location, let it create it once...it will fail again. However, if you switch that storage "container" from Private to BLOB - it will fix the problem and you can continually deploy to that location for your artifacts.
  • Anonymous
    January 18, 2017
    The comment has been removed
    • Anonymous
      January 18, 2017
      Hi KyleIt might help if you provided the specific error message. Do you mean ps1 or psd1?CheersMark
  • Anonymous
    February 05, 2017
    Hi Mark,Great writeup!Any thoughts on how to deploy a "Read-Only" domain controller?
  • Anonymous
    April 15, 2017
    I keep getting error:xADDomain Converting and storing encrypted passwords as plain text is not recommended. For more information on securing credentials in MOF file, please refer to MSDN blog: http://go.microsoft.com/fwlink/?LinkId=393729
    • Anonymous
      April 16, 2017
      Hi HosseinThe steps under Configuration Data for DSC should address this. You really should read the references provided in the error message and use a certificate to protect the password but if you're just setting up a proof of concept, you can bypass the protection of your admin credentials using the configuration data file as I've described. I've seen the error you're getting when the configuration data file wasn't uploaded to Azure storage.
      • Anonymous
        April 17, 2017
        Hi MarkI checked, the configuration data file is uploaded to azure storage. If I change the Node name to 'localhost' it will work, otherwise it throw the error
  • Anonymous
    June 15, 2017
    The comment has been removed
  • Anonymous
    July 18, 2017
    Hi,I am trying to run a DSC which updates the WMF on windows server. I am getting the below error :Downloading https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7AndW2K8R2-KB3191566-x64.zip to C:\Packages\Plugins\Microsoft.Powershell.DSC\2.26.1.0\bin..\DSCWork\Win7AndW2K8R2-KB3191566-x64.zipVERBOSE: [2017-06-21 09:11:26Z] Download failed: Exception calling "DownloadFile" with "2" argument(s): "The underlying connection was closed: An unexpected error occurred on a send.From the log output, it is obvious that Win7AndW2K8R2-KB3191566-x64.zip is not getting downloaded.But i am not sure what is wrong. Is there any configuration or setting which would help?Any solution or suggestion will be of great help.Thanks in advance
    • Anonymous
      July 18, 2017
      Somewhat unrelated to this blog post and I'm making a guess but I'd start with whether the download target location is accessible and following that, start network analysis with Message Analyzer or Wireshark. "The underlying connection was closed" may indicate a network layer issue. Other than that, I'd be looking for appropriate log files with a greater level of insight.
      • Anonymous
        July 19, 2017
        Hi Mark, Thanks for the info.
  • Anonymous
    September 04, 2017
    Hi, I've been following it along for newbie educational purposes, but I get stuck at the last section where a parameter has not been defined and gives a red underline to show an error:"forceUpdateTag": "[parameters('dscDCUpdateTagVersion')]",I cannot find where you have defined dscDCUpdateTagVersion, and obviously the deployment won't proceed. Any help on this would be appreciated.
  • Anonymous
    September 10, 2017
    Hi - I've followed the steps in this faithfully, and keep hitting a problem that when the new server goes to download its .PSD1 it does not use the credentials which it uses to download the ZIP .. i.e. the is created in the DSC folder beside the PS1 and PSD1 files on the machine where I run visual studio and DSC folder is created in the Azure container (I can see both files in it using Azure storage explorer). The zip is downloaded and the modules it installed, and the PSD1 fails To get around this modified the Powershell code where New-AzureStoragecontainer runs so it ends | Set-AzureStorageContainerAcl -Permission BlobNot sure what has changed to cause this, but hopefully the work round will help someone
    • Anonymous
      September 10, 2017
      One thing that's clear since I started blogging about Azure matters is that things change all the time and that the validity of a post won't stand the test of time. I hope that the information I share takes readers at least most of the way to their goal. And if it doesn't, at least you learn something from hitting your own issues! :)