Build an on-premises Domain Controller (DC) using Desired State Configuration

In my last post I described how to create a DC, in Azure, using DSC and ARM Templates.  In this post, we will discuss how to automate the creation of a local – on premises – Acitve Directory Domain Services (ADDS) Domain Controller (DC) using DSC.

Overview

DSC has two modes: push mode and pull mode.

In push mode, you will author the configuration.  You will then stage the configuration by creating MOF files.  And finally, you will manually push the desired configuration onto the target server or node.  The target server can be the local server or a remote server.

On the other hand, in DSC PULL mode, you author the configuration and stage it onto a designated Pull server.  The target nodes contact the central pull server at regular intervals to obtain their desired configuration.

In our scenario, we will be using DSC in push mode.  We will author the configuration and push it onto the local Windows server (not remotely).

Details

Prerequisites

On the target Windows Server (Windows Server 2008 R2 SP1 and Windows Server 2012 or 2012 R2):

  1. Download and install Windows Management Framework (WMF).  This WMF 5.0 is currently available and is the recommended version.
  2. Copy the script below to the target server
  3. Open the script below in Powershell ISE as administrator
  4. Install the required Powershell modules using install-module: xActiveDirectory, xComputerManagement, xNetworking and xStorage.

Run the script

Run the script in Powershell ISE.  The first command creates the .mof files which contain the desired configuration.  The second command actually applies the configuration to the local server.  After about half an hour and one reboot, you will have a fully functional Domain Controller with a new user (domain admin).

# Configure all of the settings we want to apply for this configuration
$ConfigData = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            MachineName = 'spfarm-ad'
            IPAddress = '10.0.0.4'
            InterfaceAlias = 'Ethernet'
            DefaultGateway = '10.0.0.1'
            PrefixLength = '24'
            AddressFamily = 'IPv4'
            DNSAddress = '127.0.0.1', '10.0.0.4'
            PSDscAllowPlainTextPassword = $true
            PSDscAllowDomainUser = $true
        }
    )
}

Configuration BuildADDC {

    param (
        [Parameter(Mandatory)]
        [String]$FQDomainName,

        [Parameter(Mandatory)]
        [PSCredential]$DomainAdminstratorCreds,

        [Parameter(Mandatory)]
        [PSCredential]$AdmintratorUserCreds,

        [Int]$RetryCount=5,
        [Int]$RetryIntervalSec=30
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName xActiveDirectory, `
                                    xComputerManagement, `
                                    xNetworking,
									xStorage
 
    Node $AllNodes.NodeName 
    {
        LocalConfigurationManager 
        {
            ActionAfterReboot = 'ContinueConfiguration'            
            ConfigurationMode = 'ApplyOnly'            
            RebootNodeIfNeeded = $true  
        }

        # Change Server Name
        xComputer SetName { 
          Name = $Node.MachineName 
        }

        # Networking
        xDhcpClient DisabledDhcpClient
        {
            State          = 'Disabled'
            InterfaceAlias = $Node.InterfaceAlias
            AddressFamily  = $Node.AddressFamily
        }

        xIPAddress NewIPAddress
        {
            IPAddress      = $Node.IPAddress
            InterfaceAlias = $Node.InterfaceAlias
            PrefixLength   = $Node.PrefixLength
            AddressFamily  = $Node.AddressFamily
        }

        xDefaultGatewayAddress SetDefaultGateway
        {
            Address        = $Node.DefaultGateway
            InterfaceAlias = $Node.InterfaceAlias
            AddressFamily  = $Node.AddressFamily
            DependsOn = '[xIPAddress]NewIPAddress'
        }
       
        xDNSServerAddress SetDNS {
            Address = $Node.DNSAddress
            InterfaceAlias = $Node.InterfaceAlias
            AddressFamily = $Node.AddressFamily
        }

        # Install the Windows Feature for AD DS
        WindowsFeature ADDSInstall {
            Ensure = 'Present'
            Name = 'AD-Domain-Services'
        }

        # Make sure the Active Directory GUI Management tools are installed
        WindowsFeature ADDSTools            
        {             
            Ensure = 'Present'             
            Name = 'RSAT-ADDS'             
        }           

        # Create the ADDS DC
        xADDomain FirstDC {
            DomainName = $FQDomainName
            DomainAdministratorCredential = $DomainAdminstratorCreds
            SafemodeAdministratorPassword = $DomainAdminstratorCreds
            DependsOn = '[xComputer]SetName','[xDefaultGatewayAddress]SetDefaultGateway','[WindowsFeature]ADDSInstall'
        }   
        
        $domain = $FQDomainName.split('.')[0] 
        xWaitForADDomain DscForestWait
        {
            DomainName = $domain
            DomainUserCredential = $DomainAdminstratorCreds
            RetryCount = $RetryCount
            RetryIntervalSec = $RetryIntervalSec
            DependsOn = '[xADDomain]FirstDC'
        } 

        #
        xADRecycleBin RecycleBin
        {
           EnterpriseAdministratorCredential = $DomainAdminstratorCreds
           ForestFQDN = $domain
           DependsOn = '[xADDomain]FirstDC'
        }
        
        # Create an admin user so that the default Administrator account is not used
        xADUser FirstUser
        {
            DomainAdministratorCredential = $DomainAdminstratorCreds
            DomainName = $domain
            UserName = $AdmintratorUserCreds.UserName
            Password = $AdmintratorUserCreds
            Ensure = 'Present'
            DependsOn = '[xADDomain]FirstDC'
        }
        
        xADGroup AddToDomainAdmins
        {
            GroupName = 'Domain Admins'
            MembersToInclude = $AdmintratorUserCreds.UserName
            Ensure = 'Present'
            DependsOn = '[xADUser]FirstUser'
        }
        
    }
}

# Build MOF (Managed Object Format) files based on the configuration defined above 
# (in folder under current dir) 
# Local Admin is assigned 
BuildADDC -ConfigurationData $ConfigData `
          -FQDomainName 'spdomain.local' `
          -DomainAdminstratorCreds (get-credential -Message "Enter Admin Credentials" -UserName "Administrator" ) `
          -AdmintratorUserCreds (get-credential -Message "Enter New Admin User Credentials" -UserName "admin1" ) 

# We now enforce the configuration using the command syntax below
Start-DscConfiguration -Wait -Force -Path .\BuildADDC -Verbose -Debug

Lessons Learned

Since the Powershell xActiveDirectory module is being updated all the time, a DSC script that worked a year ago needs to be updated to work with WMF 5.0 (in the last quarter of 2016).

WMF 5.0 is included in the latest version of Windows 10 and included on Windows Server 2016.

Issues and Solutions

With some of the xActiveDirectory resources, the use of the fully qualified domain name (FQDN) produced an error: “Could not find mandatory property DomainName. Add this property and try again.”

Solution: use the first part of the domain name

Building an ADDS DC in Azure IAAS using ARM and DSC

Sometimes we may need to create an Active Directory Domain Services Domain Controller (DC) in an Azure IAAS Virtual Machine (VM).  For example, I recently set up an IAAS SharePoint farm in the cloud.  A DC on an Azure IAAS VM was a natural fit.

This post will discuss the steps required to deploy and provision such a DC in Azure – using Azure Resource Manager (ARM) templates combined with Powershell Desired State Configuration (DSC).

Brief Introduction to ARM templates and to Powershell Desired State Configuration (DSC)

ARM and DSC are tools that allow the description of infrastructure as code.  Infrastructure described in code allows you to deploy infrastructure consistently.  And it makes the build and deployment process repeatable.

ARM templates

ARM templates are JSON files that describe the resources (infrastructure components) that you need to deploy to Azure.  For example, a VM, a Web App, a VNET or a NIC etc.

DSC

On the other hand, Powershell DSC allows you to describe the end state of how you like your – on premises or Cloud – server to look like.  Once you have described your configuration with all the needed roles and features, DSC goes ahead and “makes it so”.  In other words, it provisions the server per your specifications.

ARM Templates & DSC

Both ARM templates and Powershell DSC provide the necessary tools for consistently deploying applications.  And consistently deployed applications are critical factors needed for DevOps.

Steps to create a DC in Azure

Overview of the Process
  1. Create a new Azure Resource Group project is VS.
  2. Add the Windows Virtual Machine template to the project.
  3. Subsequently, add the DSC Extension resource.
  4. A new DSC configuration data file (.psd1 aka Powershell Script data file) is added to the project.  Alternatively, the PS data file may be hosted online.
  5. Customize (edit) your JSON template files and your DSC files.
  6. Deploy the solution to Azure.
  7. Check on your Virtual Machine in Azure.  Remote Desktop to the VM and verify that ADDS, DNS and ADDS Recycle Bin roles and features are enabled.
Detailed Steps:

1. I created a New Project in Visual Studio and chose Azure Resource Group for the type:

2. I chose Windows Virtual Machine as the Template:

choose Azure Virtual Machine
choose Azure Virtual Machine

3. I opened the windowsvirtualmachine.json file.  In the JSON outline (left-hand side), I right-clicked on Resources and added a new resource – PowerShell DSC Extension:

Open VM json file

4. I added the Powershell Data file (.psd1).  However, I ended up not using it because the deployment script did not find it (see details below).  Instead, I uploaded the data file on Github and used a link (URL) to it in the JSON template.

add Powershell data file

5. I modified the WindowsVirtualMachine.json file adding the parameters and variables that will be used in the JSON document:

  "parameters": {
    "adminUsername": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Username for the Virtual Machine."
      }
    },
    "adminPassword": {
      "type": "securestring",
      "metadata": {
        "description": "Password for the Virtual Machine."
      }
    },
    "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"
      }
    },
    "DSC-VMDCUpdateTagVersion": {
      "type": "string",
      "defaultValue": "1.0",
      "metadata": {
        "description": "This value must be changed from a previous deployment to ensure the extension will run"
      }
    },
    "sizeOfDataDiskInGB": {
      "type": "int",
      "defaultValue": 100,
      "metadata": {
        "description": "Size of each data disk in GB"
      }
    },
    "FQDomainName": {
      "type": "string",
      "metadata": {
        "description": "Domain name to give to new ADDS Domain"
      }
    },
    "AdminUser1CredsParam": {
      "type": "securestring",
      "metadata": {
        "description": "Admin User password"
      }
    }
  },
  "variables": {
    "imagePublisher": "MicrosoftWindowsServer",
    "imageOffer": "WindowsServer",
    "OSDiskName": "osdiskforwindowssimple",
    "DataDiskName": "datadisk1nocache",
    "nicName": "myVMNic",
    "addressPrefix": "10.0.0.0/16",
    "subnetName": "Subnet",
    "subnetPrefix": "10.0.0.0/24",
    "vhdStorageType": "Standard_LRS",
    "publicIPAddressName": "myPublicIP",
    "publicIPAddressType": "Dynamic",
    "vhdStorageContainerName": "vhds",
    "vmName": "ARMDCVM",
    "vmSize": "Standard_A2",
    "virtualNetworkName": "MyVNET",
    "vnetId": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetId'), '/subnets/', variables('subnetName'))]",
    "vhdStorageName": "[concat('vhdstorage', uniqueString(resourceGroup().id))]",
    "diagnosticsStorageAccountName": "[variables('vhdStorageName')]",
    "diagnosticsStorageAccountResourceGroup": "[resourcegroup().name]",
    "accountid": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', variables('diagnosticsStorageAccountResourceGroup'), '/providers/', 'Microsoft.Storage/storageAccounts/', variables('diagnosticsStorageAccountName'))]",
    "wadmetricsresourceid": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', variables('diagnosticsStorageAccountResourceGroup'), '/providers/', 'Microsoft.Compute/virtualMachines/', variables('vmName'))]",
    "DSC-VMDCArchiveFolder": "DSC",
    "DSC-VMDCArchiveFileName": "armdscvmdc.zip",
    "DSC-ConfigFile": "armdscvmdc.ps1",
    "DSC-ConfigDataFile": "armdscvmdc.psd1"
  },

6. I edited the Virtual Machine resource section to add a Data Disk.  A second disk (with caching off) is required for Domain Controllers in Azure.  ADDS database and SYSVOL must NOT be stored on an Azure OS disk type.

          "dataDisks": [
            {
              "name": "datadisk1",
              "diskSizeGB": "[parameters('sizeOfDataDiskInGB')]",
              "lun": 0,
              "vhd": {
                "uri": "[concat('https://', variables('vhdStorageName'), '.blob.core.windows.net/', variables('vhdStorageContainerName'), '/', variables('DataDiskName'), '.vhd')]"
              },
              "createOption": "Empty",
              "caching": "None"
            }
          ]

7. Subsequently, I edited DSC section of the JSON template to include: configurationArguments (parameters passed to the DSC script) and ConfigurationData (URL to .psd1 file on Github)

        {
          "name": "Microsoft.Powershell.DSC",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-06-15",
          "dependsOn": [
            "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
          ],
          "tags": {
            "displayName": "DSC-VMDC"
          },
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.9",
            "autoUpgradeMinorVersion": true,
            "forceUpdateTag": "[parameters('DSC-VMDCUpdateTagVersion')]",
            "settings": {
              "configuration": {
                "url": "[concat(parameters('_artifactsLocation'), '/', variables('DSC-VMDCArchiveFolder'), '/', variables('DSC-VMDCArchiveFileName'))]",
                "script": "[variables('DSC-ConfigFile')]",
                "function": "Main"
              },
              "configurationArguments": {
                "nodeName": "[variables('vmName')]",
                "FQDomainName": "[parameters('FQDomainName')]"
              },
              "configurationData": {
                "url": "https://raw.githubusercontent.com/GhassanHariz/AzureRMDSC/master/DCVM-DSC.psd1"
              }
            },
            "protectedSettings": {
              "configurationArguments": {
                "DomainAdmin1Creds": {
                  "userName": "[parameters('adminUsername')]",
                  "password": "[parameters('adminPassword')]"
                },
                "AdminUser1Creds": {
                  "userName": "AdminUser1",
                  "password": "[parameters('AdminUser1CredsParam')]"
                }
              },
              "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]"
            }
          }
        }

8. The parameters list was updated in WindowsVirtualMachines.parameters.json file:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "value": "srvradmin"
    },
    "dnsNameForPublicIP": {
      "value": "dscvmdc"
    },
    "windowsOSVersion": {
      "value": "2012-R2-Datacenter"
    },
    "sizeOfDataDiskInGB": {
      "value": 100
    },
    "DSC-VMDCUpdateTagVersion": {
      "value": "2.0"
    },
    "FQDomainName": {
      "value": "spdomain.local"
    }

  }
}

9. When the DSC Extension is added to your project, the DSC script provided contains an example configuration.  Since we are creating our own configuration, that example was removed and the following was added.  The following DSC configuration provisions the DC and adds an administrative user account.

Configuration Main
{
	Param(
		[string] $NodeName,

		[Parameter(Mandatory)]
		[String]$FQDomainName,

		[Parameter(Mandatory)]
		[PSCredential]$DomainAdmin1Creds,

		[Parameter(Mandatory)]
		[PSCredential]$AdminUser1Creds,

		[Int]$RetryCount=5,
		[Int]$RetryIntervalSec=30
		)

Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xActiveDirectory, `
                                    xComputerManagement, `
                                    xNetworking,
									xStorage
 
Node $AllNodes.Where{$_.Role -eq "DC"}.Nodename
       {
		LocalConfigurationManager 
        {
            ActionAfterReboot = 'ContinueConfiguration'            
            ConfigurationMode = 'ApplyOnly'            
            RebootNodeIfNeeded = $true  
			AllowModuleOverwrite = $true
        }

        xWaitforDisk Disk2
        {
             DiskNumber = 2
             RetryIntervalSec =$RetryIntervalSec
             RetryCount = $RetryCount
        }

        xDisk ADDataDisk
        {
            DiskNumber = 2
            DriveLetter = 'F'
        }

		# Add DNS
		WindowsFeature DNS
        {
            Ensure = "Present"
            Name = "DNS"
        }

        # Install the Windows Feature for AD DS
        WindowsFeature ADDSInstall {
            Ensure = 'Present'
            Name = 'AD-Domain-Services'
        }

        # Make sure the Active Directory GUI Management tools are installed
        WindowsFeature ADDSTools            
        {             
            Ensure = 'Present'             
            Name = 'RSAT-ADDS'             
        }           

        # Create the ADDS DC
        xADDomain FirstDC {
            DomainName = $FQDomainName
            DomainAdministratorCredential = $DomainAdmin1Creds
            SafemodeAdministratorPassword = $DomainAdmin1Creds
			DatabasePath = 'F:\NTDS'
            LogPath = 'F:\NTDS'
            SysvolPath = 'F:\SYSVOL'
            DependsOn = '[WindowsFeature]ADDSInstall'
        }   
        
        xWaitForADDomain DscForestWait
        {
            DomainName = $FQDomainName
            RetryCount = $RetryCount
            RetryIntervalSec = $RetryIntervalSec
            DependsOn = '[xADDomain]FirstDC'
        } 

        #
        xADRecycleBin RecycleBin
        {
           EnterpriseAdministratorCredential = $DomainAdmin1Creds
           ForestFQDN = $FQDomainName
           DependsOn = '[xADDomain]FirstDC'
        }
        
        # Create an admin user so that the default Administrator account is not used
        xADUser FirstUser
        {
            DomainAdministratorCredential = $DomainAdmin1Creds
            DomainName = $FQDomainName
            UserName = $AdminUser1Creds.UserName
            Password = $AdminUser1Creds
            Ensure = 'Present'
            DependsOn = '[xADDomain]FirstDC'
        }
        
        xADGroup AddToDomainAdmins
        {
            GroupName = 'Domain Admins'
            MembersToInclude = $AdminUser1Creds.UserName
            Ensure = 'Present'
            DependsOn = '[xADUser]FirstUser'
        }     
    }

}

10. The DSC configuration data file (.psd1) on Github contains:

# Configure all of the settings we want to apply for this configuration
@{
    AllNodes = @(
        @{
            NodeName = '*'
            PSDscAllowPlainTextPassword = $true
            PSDscAllowDomainUser = $true
        },
        @{ 
            Nodename = "localhost"
            Role = "DC"
        }
    )
}

11. Once all the JSON and DSC files are customized, the project can be deployed to Azure.  Right-click on the project and select Deploy-New.  Choose your Azure account and subscription.  Verify the parameters entered and click OK to deploy to the Azure Resource Group.

12. After about 30 minutes, the deployment will be completed.  Log in to the Azure portal and verify your DC is up and running.  You can Connect to the DC via Remote Desktop to view your newly created DC

Domain Controller Azure

Lessons learned

Why use Visual Studio?

Microsoft Visual Studio 2015 Community Edition is a wonderful development IDE and it is quite good for authoring ARM templates and the DSC scripts.  It goes ahead and deploys the whole solution to Azure and allows you to monitor the progress and catch any errors.

The advantage of VS for ARM and DSC

You can author the ARM templates and DSC scripts in any text editor or IDE and then deploy them using Powershell.  However, in my experience, deploying the templates and DSC code from VS provided more feedback about the progress of the deployment.  I do not have to add any parameters for verbose output and I do not have to check any log files.

Furthermore, VS is suitable for PowerShell DSC because you can add a VS Powershell extension (from the Visual studio Marketplace), that allows you to author and edit PS code.

Issues and Solutions

DSC Configuration Data File not being found during deployment

As mentioned above the .psd1 was originally added to the VS project.  Once it is added to VS, you have to right-click on the .psd1 file and select Properties.  For All Configurations select:

  • Build Action : Always
  • Copy to Ouput Directory: Copy Always

When I tried to change the Build Action, I received an error: An error occurred while saving the edited properties listed below: Build Action.  One or more values are invalid.  Mismatched PageRule with the wrong ItemType.

The workaround is to apply the second setting first (Copy Always).  Then apply the first setting (Build Action).

The above workaround was good and allowed the .psd1 file to be copied to the correct location in the staging area along with the .ps1 file and the Zip archive.  However, the deployment process still complained that the .psd1 file was not found “after 19 attempts”.

Online search on this error resulted in posts suggesting that a duplicate path in the PSModulePath environment variable may be the culprit.  However, none of the workarounds and suggestions allowed the .psd1 file to be found.

Workaround: place the .psd1 file online.  I placed the file on Github in order to get it deployed.