03 Jan 2018

Configure an Ansible testing system on Windows (Part 3)

This is the final part of a three-part series.  In the first entry of this series, we configured Vagrant and built our basic inventory for Ansible.   We then used Ansible to create a common role with basic tasks that we need to complete on all hosts.   Now we will create specialized playbooks to create both our CONTOSO.com domain controller and create a basic member file server for the domain.

Building the CONTOSO.com Domain

For our domain controller setup, we will check if the CONTOSO.com domain exists, create it if it doesn't but join the domain if it does.  Now our test environment does not contain a CONTOSO.com domain controller out of the box, so we can expect that the domain won't exist.  Typically, however, you want more than one domain controller, so this playbook will be able to handle additional controllers joining the domain.  Open the domain_controller.yml and below the roles section add a new tasks section with the following as our first steps:

  tasks:
  # ensure the named domain is reachable from the target host; 
  # if not, create the domain in a new forest residing on the target host
  - name: Ensure that CONTOSO.com Domain exists
    win_domain:
      dns_domain_name: CONTOSO.com
      safe_mode_password: AutomationDoesW0rk!
    register: check_domain

  # Creating a Domain Controller requires a reboot
  - name: Reboot to complete CONTOSO.com domain setup
    win_reboot:
      shutdown_timeout: 600
      reboot_timeout: 600
      post_reboot_delay: 300
    when: check_domain.changed

We use post_reboot_delay: 300 to give the server 5 minutes post reboot to complete setup and be ready to execute additional steps.   Your system's performance may vary and additional time at this step may be required, adjust as needed.

The second step we've seen before, it's just a simple reboot.  The uses Ansible's win_domain module to ensure that CONTOSO.com exists.   If the domain does not exist the module will enable the required Windows features to support the server acting as a domain controller, create the domain, and set the safe_mode password for the domain controller.   We register check_domain to store the results of this step in order to determine if we need to reboot to complete setup.

Next, we'll add the steps required to join CONTOSO.com if the domain already exists.   For these steps, we'll be supplying a domain_admin username and password as well as a safe_mode_password for the domain controller.   We will use values similar to the previous steps, add these new steps to the end of the file:

  - name: Ensure the server is a domain controller
    win_domain_controller:
      dns_domain_name: CONTOSO.com
      domain_admin_user: test_admin@CONTOSO.com
      domain_admin_password: AutomationDoesW0rk!
      safe_mode_password: AutomationDoesW0rk!
      state: domain_controller
      log_path: c:\ansible_win_domain_controller.txt
    register: check_domain_controller

  # Creating a Domain Controller requires a reboot
  - name: Reboot to complete domain controller setup
    win_reboot:
      shutdown_timeout: 600
      reboot_timeout: 600
      post_reboot_delay: 300
    when: check_domain_controller.changed

Now that we have CONTOSO.com Active Directory domain and a new Domain Controller up and running we have some final adjustments to make.  We should create an additional domain administrator. Add the following step to the end of the file to create a domain administrator:

  - name: Ensure that Domain Admin test_admin@CONTOSO.com is present in OU cn=Users,dc=CONTOSO,dc=com
    win_domain_user:
      name: test_admin
      password: AutomationDoesW0rk!
      state: present
      path: cn=Users,dc=CONTOSO,dc=com
      groups:
        - Domain Admins

Once this server became a domain controller it's DNS configuration no longer allows it to access the internet resources since it's not configured with forwarders.  It can only locate resources in CONTOSO.com, which given that it's all alone isn't much.  So we need to setup some DNS forwarding and use Google's public DNS servers.   For this, we are again going to take advantage of Powershell DSC this time using the xDNSServer module.  Add the following steps to complete this configuration:

  - name: Check for xDnsServer Powershell module
    win_psmodule:
      name: xDnsServer
      state: present
  - name: Configure DNS Forwarders
    win_dsc:
      resource_name: xDnsServerSetting
      Name: DNSServerProperties
      NoRecursion: false
      Forwarders:
        - "8.8.8.8"
        - "8.8.4.4"

Since we don't want two servers trying to build out the domain at the same time, we'll enable serial processing for this runbook to ensure we work with them one by one.  This way both nodes don't try to create CONTOSO.com if it doesn't exist.   At the top of the file under hosts: domain_controllers add the following line:

serial: 1

That's it!  Here's the full playbook for reference:

---
- name: CONTOSO.com Domain Controller configuration
  hosts: domain_controllers
  serial: 1

  roles:
    - { role: common }

  tasks:

  # ensure the named domain is reachable from the target host; if not, create the domain in a new forest residing on the target host
  - name: Ensure that CONTOSO.com Domain exists
    win_domain:
      dns_domain_name: CONTOSO.com
      safe_mode_password: AutomationDoesW0rk!
    register: check_domain

  # Creating a Domain Controller requires a reboot
  - name: Reboot to complete CONTOSO.com domain setup
    win_reboot:
      shutdown_timeout: 600
      reboot_timeout: 600
      post_reboot_delay: 300
    when: check_domain.changed

  - name: Ensure the server is a domain controller
    win_domain_controller:
      dns_domain_name: CONTOSO.com
      domain_admin_user: test_admin@CONTOSO.com
      domain_admin_password: AutomationDoesW0rk!
      safe_mode_password: AutomationDoesW0rk!
      state: domain_controller
      log_path: c:\ansible_win_domain_controller.txt
    register: check_domain_controller

  # Creating a Domain Controller requires a reboot
  # Long delay since the DC setup can take a while.
  - name: Reboot to complete domain controller setup
    win_reboot:
      shutdown_timeout: 600
      reboot_timeout: 600
      post_reboot_delay: 300
    when: check_domain_controller.changed

  - name: Check for xDnsServer Powershell module
    win_psmodule:
      name: xDnsServer
      state: present

  - name: Configure DNS Forwarders
    win_dsc:
      resource_name: xDnsServerSetting
      Name: DNSServerProperties
      NoRecursion: false
      Forwarders:
        - "8.8.8.8"
        - "8.8.4.4"

  - name: Ensure that Domain Admin test_admin@CONTOSO.com is present in OU cn=Users,dc=CONTOSO,dc=com
    win_domain_user:
      name: test_admin
      password: AutomationDoesW0rk!
      state: present
      path: cn=Users,dc=CONTOSO,dc=com
      groups:
        - Domain Admins

Let's run this playbook and get CONTOSO.com up and running:

ansible-playbook domain_controller.yml -i environments/test/hosts

Occasionally a domain related task (such Domain Admin creation) will fail due to timing issues on the virtual machine.  I've tried to add enough post_reboot_delay to get around this but timing varies greatly based on your system performance.  So if the playbook fails it may simply be a performance issue on the VM, give it a re-run and usually it picks up and works.

All set! Now it's time to add a member server to our test environment.

Creating a member server in CONTOSO.com

Our example member server will be a simple file server with a share called Users, nothing special.    First, we need to get it setup so that it's DNS is reconfigured and can find a CONTOSO.com domain controller.   Create a new playbook called member_server.yml and create a new task to reconfigure the DNS Server Address:

---

- name: CONTOSO.com member server configuration
  hosts: member_servers

  roles:
    - { role: common }

  tasks:

  - name: Configure DNS Servers
    win_dsc:
      resource_name: xDnsServerAddress  
      Address: 192.168.100.10
      InterfaceAlias: Ethernet
      AddressFamily: IPv4
      Validate: $true

This Powershell DSC is part of the xNetworking module since we already used it in the common role we don't need to check that the module is available first.   If you've adjusted the IP addresses we used in the vagrant file, you will need to make similar adjustments here as well.   This step configures our member server to use our newly created domain controller as a DNS server which is required for it to find our new CONTOSO.com domain.

Next, we need to ensure that the proper windows features for acting as a file server are installed on our host.  Add this task to the playbook:
  - name: Verify File Server Role is installed.
    win_feature:
      name: File-Services, FS-FileServer
      state: present
      include_management_tools: True

This step uses the win_feature module to execute the Add/Remove-WindowsFeature Cmdlets and install the required items.  You can retrieve a complete list of available windows features for a particular Windows Server edition by executing the following PowerShell command on the server:

Get-WindowsFeature

The name column in the output will contain the values that Ansible's win_feature module needs.

Our final task is to create a basic share on our member server.  For this example, we'll create a basic public folder that any domain user can add files to.   This is a two-step process, add the following tasks to the end of the playbook:

  - name: Ensure directory structure for public share exists
    win_file:
      path: C:\shares\public
      state: directory

  - name: Ensure public share exists
    win_share:
      name: public
      description: Basic RW share for all domain users
      path: C:\shares\public
      list: yes
      full: Administrators
      change: Users

These steps are pretty straightforward, first, we make sure the folder structure we are planning to share exists and then we create a new share.

Execute the playbook with the following command to configure your member server:

ansible-playbook member_server.yml -i environments/test/hosts

If all goes well you should be able to RDP into the virtual environment and access the newly created share.   We've successfully created a local test environment complete with a domain controller and basic member file server.  Using this we now have a safe space to develop and test additional playbooks.   In addition, if the environment ever gets damaged or we would like to reset it to this state we can do so with a few simple commands to restore it to this state:

vagrant destroy
vagrant up
ansible-playbook domain_controller.yml -i environments/test/hosts
ansible-playbook member_server.yml -i environments/test/hosts

While this guide has barely scratched the surface of what is possible with Ansible, hopefully, it has helped you on the path to improving your Windows configuration management.