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.

Comments

  • Anonymous
    July 25, 2016
    Here i had read the content you had posted. It is much interesting so please keep update like this.
  • Anonymous
    July 25, 2016
    Really an amazing post..! By reading your blog post i gained more information. Thanks a lot for posting unique information and made me more knowledgeable person. Keep on blogging!!
  • Anonymous
    July 27, 2016
    Good stuff. Thanks Mark!
  • Anonymous
    September 18, 2016
    Thanks for a great article, but I'm getting stuck at one point:Your script contains the following:xADDomain CreateForest Is this referring to another function or script that isn't in the blog entry?Thanks
    • Anonymous
      September 18, 2016
      Hi RobThanks for commenting. I re-read my post and realised I forgot to mention that you need the xActiveDirectory DSC module installed for this to work. I added an edit to the post that mentions this.If you're on PS 5.0 or later, just run Install-Module xActiveDirectory on your tools machine and that should sort it out for you.
  • Anonymous
    September 19, 2016
    Hi Mark,To add a powershell datafile, you need the Powershell VS plugin installed. Otherwise it doesn't show up as an option while adding a new item.
    • Anonymous
      September 19, 2016
      Thanks for the catch. As I mentioned at the start of the post, I used one of the pre-canned MSDN Azure VMs that has everything you need already set up so I didn't spend time on the details.
  • Anonymous
    November 04, 2016
    Great post. I have a small issue though. When following the steps I end up with a missing variable: "configurationFunction": "[variables('dscDCConfigurationFunction')]" I can see, that you have 27 variables in your solution, but mine ends up with only 23. configurationFunction is not among them. What should configurationFunction be set to?
    • Anonymous
      November 05, 2016
      Hi AxelInfrastructure as code - repeatable and reusable until they change something - which is all the time. Since I wrote this post, the DSC resource .json has been updated and it doesn't seem to work in the same way. When I authored this you'd get something like the following in the DSC .json - "settings": { "modulesUrl": "[concat(parameters('_artifactsLocation'), '/', 'dscDC.ps1.zip')]", "sasToken": "[parameters('_artifactsLocationSasToken')]", "configurationFunction": "[variables('dscDCConfigurationFunction')]",And I'd have variables ... "dscDCArchiveFolder": "DSC", "dscDCArchiveFileName": "dscDC.zip", "dscDCConfigurationFunction": "dscDC.ps1\Main"If I create a new project and repeat the steps, now I see ... "settings": { "configuration": { "url": "[concat(parameters('_artifactsLocation'), '/', variables('dscDCArchiveFolder'), '/', variables('dscDCArchiveFileName'))]", "script": "dscDC.ps1", "function": "Main" },with variables "dscDCArchiveFolder": "DSC", "dscDCArchiveFileName": "dscDC.zip"I'll have to find some time to run through this from start to finish again and update this post. Hopefully this gives you enough to get it going in the mean time.CheersMark
      • Anonymous
        November 21, 2016
        Your edits for Deploy-Azureresourcegroup.ps1 may no longer be required, because If you place a file in the DSC folder alongside the other scripts and right click the .psd1 file, select properties "build action|Content" it will automatically upload the file to the blob storage for you.I'm not sure where the databloburi part should go now though as the template has changed in that area as well now.
  • Anonymous
    November 05, 2016
    Hi MarkThanks for this article, it's very good. But I have an error: the DSC can't create the mof file or psd1 file is not reachable. Can you share your solution for understanding my error.Thanks
    • Anonymous
      November 05, 2016
      The comment has been removed
  • Anonymous
    November 27, 2016
    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 16, 2017
    Your post was really helpful, but unfortunately, I am not having any success with configuring the server using dsc.
    • Anonymous
      November 16, 2017
      The comment has been removed