Close

19th June 2017

Deploy an AD Domain in IaaS to Azure with ARM and DSC

Deploy an AD Domain in IaaS to Azure with ARM and DSC

I’ll preface with a few of the resources I’ve used so far in my Azure study journey

Quick Update

I’ve been working with Ravello to build out a vSphere environment, but have had to put that on hold just for the moment to take a closer look at Azure.  I’ve been working with Azure for a while in a hands off capacity that has been rewarding in the complexity, but lacking in hands on opportunities.   A chance presented itself to get back hands on with Azure and I’ve jumped at the chance to get back hands on.

I’ve registered for the 70-533 exam with a retake and practice test bundle.  To  provide my learning some impetus, as those familiar with my TOGAF posts will have read I like setting myself a challenge.

With a more than solid grasp of the basics, I wanted to develop something a bit more meaningful that might be of continuing practical use.  I set myself the challenge of scripting the build of an AD Domain controller to Azure using ARM and DSC.  The aim here is to have a re-usable building block for a lab environment.  Something that can be used as a base for more exiting templates and configurations.

This blog post will as ever help me solidify my learning.

Tools

The basic tools you need to follow this guide are as follows;

  1. An Azure Subscription, available as a trial account (with some limitations to core usage etc. or as part of an MSDN subscription).  I used a trial account for everything that follows.
  2. Visual Studio, free community versions available from Microsoft.
  3. PowerShell ISE, available alongside a host of other goodies from Microsoft.
  4. GitHub account and a repository to host extended DSC configuration.
  5. BitBucket account and a repository for software version control.  Github charges for private repositories, BitBucket doesn’t so was a better fit for me.

PowerShell is going to need some modules importing if they are not already available.  you can see what modules are available by running ‘get-module‘.  run that command looking for ‘Microsoft.Powershell.Management‘ and ‘xActiveDirectory‘ in the output.  if they are not listed, the modules need to be imported.  At this point you can either download them to a known location and add them via; ‘import-module -name Microsoft.PowerShell.Management‘ or ‘import-module  -name xActiveDirectory‘ accordingly.

Or you can get them from the PowerShell gallery.  in order to bring them in from that source you need to set the source to trusted.  This can be done via the following command ‘Set-PSRepository -Name PSGallery -InstallationPolicy Trusted‘. Working with a trusted repository, the modules can now be installed using ‘install-module -name xActiveDirectory‘ etc.

Getting started in Visual Studio

To get started we want to create a new Azure Resource Group project from within Visual Studio.  To do that;

  1. Open Visual Studio (run as administrator).
  2. File > New > Project.
  3. Underneath Installed > Templates > Visual c# > Cloud & Azure Resource Group
  4. Give the project a name and a solution name.

Deploy an AD Domain in IaaS to Azure with ARM and DSC

The new project wizard is then going to allow you to select the type of resource that you want to deploy into Azure.  Selecting a template will give you all of the files that need to be configured to script the creation of resources in Azure.

The base elements needed to deploy the server to host our new forest are contained within the ‘Windows Virtual Machine’ template.

This template creates the following files within the solution; ‘WindowsVirtualMachine.json‘, ‘WindowsVirtualMachine.parameters.json‘ and ‘Deploy-AzureResourceGroup.ps1

Deploy-AzureResurcegroup.ps1‘ – This PowerShell script references the json template and configuration data to drive the deployment of the solution to Azure.  When you deploy the solution, you are in effect running this script that calls information from the json templates and configuration data.

‘WindowsVirtualMachine.parameters.json’ – This contains the configuration data that is fed per deployment configuration data into the ARM template.

At the moment this file contains no information and will prompt the user for an administrative username and DNS name for the public interface.

WindowsVirtualMachine.json’ – This is the template.  Through manipulation of this file the VM that will be deployed can be configured and managed.   Through the addition and use of ‘copy‘ elements multiple resources can be deployed, through the extension of the template VMs can be added to existing directories.  Both of those topic deserve more in depth coverage so I will leave them for a later date.

The JSON outline contains the resources that the template is configured to deploy.  You can see with a sample template the deployment includes a Storage Account, Public IP Address, Virtual Network, Network Interface and a Virtual Machine this is configured with an extension for Azure Diagnostics.

In other words it provides all of the resources that are needed to progress with the deployment.

Desired State Configuration

Desired State Configuration (DSC) is an essential part of the configuration, management and maintenance of Windows-based servers. It allows a PowerShell script to specify the configuration of the machine using a declarative model in a simple standard way that is easy to maintain and understand.

DSC can be used to specify the configuration of the resources that are delivered in the template.  More specifically it can be used to specify the configuration of an AD forest on the ‘VirtualMachine‘ resource.

We add the DSC extension by right mouse clicking the ‘VirtualMachine‘ resource and selecting ‘add new resource‘.  This loads an ‘Add Resource‘ wizard that requests a name and the section of a Virtual Machine that it applies to.

After adding this the DSC extention will appear in the JSON outline, adding a section to the template and a PowerShell script to the solution.

The Default DSC PowerShell Script will contain the following, it’s worth having a quick look to see the sort of features that can be deployed this way.

Configuration Main
{

Param ( [string] $nodeName )

Import-DscResource -ModuleName PSDesiredStateConfiguration

Node $nodeName
  {
   # This commented section represents an example configuration that can be updated as required.
    WindowsFeature WebServerRole
    {
      Name = "Web-Server"
      Ensure = "Present"
    }
    WindowsFeature WebManagementConsole
    {
      Name = "Web-Mgmt-Console"
      Ensure = "Present"
    }
    WindowsFeature WebManagementService
    {
      Name = "Web-Mgmt-Service"
      Ensure = "Present"
    }
    WindowsFeature ASPNet45
    {
      Name = "Web-Asp-Net45"
      Ensure = "Present"
    }
    WindowsFeature HTTPRedirection
    {
      Name = "Web-Http-Redirect"
      Ensure = "Present"
    }
    WindowsFeature CustomLogging
    {
      Name = "Web-Custom-Logging"
      Ensure = "Present"
    }
    WindowsFeature LogginTools
    {
      Name = "Web-Log-Libraries"
      Ensure = "Present"
    }
    WindowsFeature RequestMonitor
    {
      Name = "Web-Request-Monitor"
      Ensure = "Present"
    }
    WindowsFeature Tracing
    {
      Name = "Web-Http-Tracing"
      Ensure = "Present"
    }
    WindowsFeature BasicAuthentication
    {
      Name = "Web-Basic-Auth"
      Ensure = "Present"
    }
    WindowsFeature WindowsAuthentication
    {
      Name = "Web-Windows-Auth"
      Ensure = "Present"
    }
    WindowsFeature ApplicationInitialization
    {
      Name = "Web-AppInit"
      Ensure = "Present"
    }
    Script DownloadWebDeploy
    {
        TestScript = {
            Test-Path "C:\WindowsAzure\WebDeploy_amd64_en-US.msi"
        }
        SetScript ={
            $source = "https://download.microsoft.com/download/0/1/D/01DC28EA-638C-4A22-A57B-4CEF97755C6C/WebDeploy_amd64_en-US.msi"
            $dest = "C:\WindowsAzure\WebDeploy_amd64_en-US.msi"
            Invoke-WebRequest $source -OutFile $dest
        }
        GetScript = {@{Result = "DownloadWebDeploy"}}
        DependsOn = "[WindowsFeature]WebServerRole"
    }
    Package InstallWebDeploy
    {
        Ensure = "Present"  
        Path  = "C:\WindowsAzure\WebDeploy_amd64_en-US.msi"
        Name = "Microsoft Web Deploy 3.6"
        ProductId = "{ED4CC1E5-043E-4157-8452-B5E533FE2BA1}"
        Arguments = "ADDLOCAL=ALL"
        DependsOn = "[Script]DownloadWebDeploy"
    }
    Service StartWebDeploy
    {                    
        Name = "WMSVC"
        StartupType = "Automatic"
        State = "Running"
        DependsOn = "[Package]InstallWebDeploy"
    }
  }
}

All of the elements are now in place that will enable the creation of the new VM and forest from script, they just need to be configured.

Editing the DSC configuration

The first edits are to add parameters for the domain name and the domain administrator credentials, so that we can pass those from the ARM template;

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

Remember earlier in the piece we installed the ‘xActiveDirectory‘ module.  This is the script that’s going to make use of it.  So lets import the modules that the script needs;

Import-DscResource -ModuleName PSDesiredStateConfiguration, xActiveDirectory

Next is to list the windows features that need to be installed for this server to functions as a domain controller.

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'
        }

The ‘Node $AllNodes.Where{$_.Role -eq “DC”}.Nodename‘ is something I hope to expand upon in another blog post.  It is a filter that will enable DSC specification to contain specs for multiple node objects.

Lastly using the imported ‘xActiveDirectory‘ module, the script can create the AD forest.

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

If that is put all together it should look something like this;

Configuration Main
{

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'
        }

  }
}

DSC Configuration Data

Best practice when working with DSC is to separate out the configuration data from the configuration itself.

That might sound like an oxymoron for what we’re doing here.  However, if you scale the solution up to using DSC to configure hundreds or thousands of servers. Separating out the configuration data simply enables better solution scaling.

To add configuration data;

  1. Right mouse click the DSC folder from solution explorer
  2. Select add > new item.
  3. Select PowerShell & PowerShell Script Data File.
  4. Give the Script Data File a name

We need this data file to be included in the build so we need to configure that.

  1. Right mouse click the PowerShell Script Data File
  2. Select Properties.
  3. Select ‘All Configurations’ from the configuration drop down.
  4. Then change the value of ‘Copy to output directory’ to  ‘Copy Always’
  5. Apply changes!
  6. Then change the value of ‘Build Action’ to ‘Content’
  7. Apply changes and ‘OK’ to exit.

If the sequence above is not followed then that might throw the following error;

If that happens revisit the sequence above and reapply the changes.

Edit the PowerShell Script Data File (*.psd1) to include the following code.

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

ARM Template changes

It’s worth noting here that when we added the DSC script and script config elements to the solution. The following variables  ‘[YourDSCName]ArchiveFolder‘ and ‘[YourDSCName]ArchiveFileName‘ were added to the ‘WindowsVirtualMachine.json‘ variables section and appropriately filed in.

in addition manual editing is required to the variables section of the ‘WindowsVirtualMachine.json‘ file is required to point toward the DSC Config Script and DSC Config File.

  "variables": {
    "imagePublisher": "MicrosoftWindowsServer",
    "imageOffer": "WindowsServer",
    "OSDiskName": "osdisk",
    "nicName": "Nic",
    "addressPrefix": "10.0.0.0/16",
    "subnetName": "VSDCLAB",
    "subnetPrefix": "10.0.0.0/24",
    "vhdStorageType": "Standard_LRS",
    "publicIPAddressName": "VSDCLAB_PUB",
    "publicIPAddressType": "Dynamic",
    "vhdStorageContainerName": "vhds",
    "vmName": "VSLABDC",
    "vmSize": "Standard_A2",
    "virtualNetworkName": "VSLAB",
    "vnetId": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetId'), '/subnets/', variables('subnetName'))]",
    "vhdStorageAccountName": "[concat('vhdstorage', uniqueString(resourceGroup().id))]",
    "diagnosticsStorageAccountName": "[variables('vhdStorageAccountName')]",
    "wadmetricsresourceid": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]",
    "dscDCArchiveFolder": "DSC",
    "dscDCArchiveFileName": "dscDC.zip",
    "DSC-ConfigFile": "dscDC.ps1",
    "DSC-ConfigDataFile": "dscDCConfigData.psd1"

To support the DSC configuration the following changes and additions need to be made to the ARM template.

As you will recall from above the ‘WindowsVirtualMachine.parameters.json‘ file is pretty empty.

Into this file we’re going to add parameters and edit the values for ‘adminUsername‘, adminPassword‘, ‘domainName‘, ‘dnsNameforPublicIP‘ and ‘windowsOSVersion‘.  These parameters should be self explanatory;

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "value": "ADMIN_NAME"
    },
    "adminPassword": {
      "value": "ADMIN_PASSWORD"
    },
    "domainName": {
      "value": "DOMAIN_NAME"
    },
    "dnsNameForPublicIP": {
      "value": "DNS_NAME"
    },
    "windowsOSVersion": {
      "value": "2012-R2-Datacenter"
    }
  }
}

We already have defined parameters for ‘adminPassword‘ and ‘windowsOSVersion‘ contained within ‘WindowsVirtualMachine.json‘.  However, we don’t have ‘domainName‘ so we need to define that within the parameters section of the file.

{
  "$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."
      }
    },
    "_artifactsLocation": {
      "type": "string",
      "metadata": {
        "description": "Auto-generated container in staging storage account to receive post-build staging folder upload"
      }
    },
    "_artifactsLocationSasToken": {
      "type": "securestring",
      "metadata": {
        "description": "Auto-generated token to access _artifactsLocation"
      }
    }
  },

The observant among you will have noticed the introduction of the following parameters ‘_artifactsLocation‘ and ‘_artifactsLocationSasToken‘ these have been added to support changes needed in the DSC extension part of the ‘WindowsvirtualMachine.json‘ file.

Lastly we need to change the DSC Extension part of ‘WindowsVirtualMachine.json‘.  We’re going to edit this section to point toward the location of the DSC files and *.psd1.

Now in theory, you can hold the *.psd1 on BLOB storage and call it from there with a secure SAS token.  I’ve not got this working. I think consensus in the community is that, certainly for test environments it is far simpler to pull this file from github.

This section of the file should now read;

{
          "name": "Microsoft.Powershell.DSC",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2016-03-30",
          "dependsOn": [
            "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
          ],
          "tags": {
            "displayName": "dscDC"
          },
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.9",
            "autoUpgradeMinorVersion": true,
            "settings": {
              "configuration": {
                "function": "Main",
                "script": "[variables('DSC-ConfigFIle')]",
                "url": "[concat(parameters('_artifactsLocation'), '/', variables('dscDCArchiveFolder'), '/', variables('dscDCArchiveFileName'))]"
              },
              "configurationArguments": {
                "domainAdminCredentials": {
                  "UserName": "[parameters('adminUserName')]",
                  "Password": "PrivateSetingsRef:Password"
                },
                "domainName": "[parameters('domainName')]",
                "nodeName": "[variables('vmName')]"
              },
              "configurationData": {
                "url": "https://raw.githubusercontent.com/****"
              }
            },
            "protectedSettings": {
              "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]",
              "items": {
                "Password": "[parameters('adminPassword')]"
              }

When linking to the *.psd1 file on github, be sure to link to the raw file format and not the *.html file presented to you initially in the repository.

Validation

Before you deploy into live you can perform validation on the project from a right mouse click of the project within Visual Studio.

Further to that I would strongly recommend validating your json using the tools at https://jsonlint.com/. These should certainly protect you from too much pain caused by fixable code errors.

Combined these two tools should enable you to highlight and fix any errors that may have cropped up in the process.

Completion

To complete simply deploy the project.

Have Fun!

Next up…

Deploy a VM and add it to an existing AD Domain in IaaS with Azure ARM and DSC.

Simon