01 Jan 2018

Configure an Ansible testing system on Windows (Part 2)

In the first entry of this series we configured Vagrant and VirtualBox to deploy a pair of basic Windows 2012 R2 virtual machines.  We built our basic inventory for Ansible and verified we could connect to our newly created virtual machines.  Now we can get onto the real work - developing Ansible playbooks.

In the first entry of this series, we configured Vagrant and VirtualBox to deploy a pair of basic Windows 2012 R2 virtual machines.  We built our basic inventory for Ansible and verified we could connect to our newly created virtual machines.  Now we can get onto the real work - developing Ansible playbooks.

Creating our common role

The first playbook files we will create are related to our common role for windows.   Essentially these are features we are going to want available on every managed system such as latest version of PowerShell.  First let's create the folder structure for our common role; create the roles folder and subfolders shown below:

In the roles/common/tasks folder create a main.yml file.   The first tasks we will add to this role will install the latest version of Powershell using the chocolatey package manager.  Since our host doesn't have chocolatey installed, Ansible will also automatically install it.   Add the following task to the file:

# The latest powershell gives us most functionality
- name: Make sure Windows Management Framework and PowerShell is latest version
  win_chocolatey:
     name: powershell
     state: latest
  register: check_powershell5
  become: yes
  become_method: runas

When we run this later, you may get a warning about the use of the runas method if you are using Ansible 2.4 due to this method being experimental.  It can safely be ignored.

The 5th line register function creates an Ansible variable that stores the results of the chocolatey task which we can use to determine what to do on later steps.   Upgrading WMF requires that we reboot the system.  So we will check to see if our Powershell5 task resulted in a change and reboot the server before continuing.   Add the following task below the first one to our common role:

# Powershell 5.0 requires a reboot, so lets get it done if it's needed.
- name: Reboot to complete Powershell 5.0 install
  win_reboot:
    shutdown_timeout: 600
    reboot_timeout: 600
    post_reboot_delay: 120
  when: check_powershell5.changed

This shutdown gives windows plenty of time - 10 minutes to shutdown, 10 minutes to reboot, followed by a 2 minute delay after boot.

If you haven't already, save the file.   Now we will create the domain controller role playbook and add the common role to it for testing.   In the top level folder ( C:\source\ansible-for-windows\ ) create a file called domain_controllers.yml.  In it, we will tell Ansible to run the playbook against the members of our [domain_controllers] group in inventory and to include the common role.

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

  roles:
  - { role: common }

Now since we are using the become_method: runas in our PowerShell step, we need a few extra variables defined to support this.   In roles\common\defaults  create main.yml and add the following values to the file:

# default user for using become: runas method
# used for installing Powershell 5.0 in this role.
ansible_become_password: vagrant
ansible_become_user: vagrant

Save the file and we can now execute our first windows playbook by typing the following command into bash:

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

This will execute our playbook and assuming everything is in order you should see this output:

Worked great, but no configuration is ever truly complete, so let's modify our common role to include enabling Remote Desktop Protocol on this host.   To do this we will take advantage of Windows Powershell Desired State Configuration, which is very powerful when used in conjunction with Ansible using the win_dsc module.  Before we can use a particular DSC module we need to the module available, luckily we can use the win_psmodule to make sure these modules are installed.

To get this setup edit the roles/common/tasks/main.yml and expand it to also complete this task by adding the following steps after our reboot for Powershell 5.0 install step:

#
# Enable Remote Desktop
#

- name: Windows | Check for xRemoteDesktopAdmin Powershell module
  win_psmodule:
    name: xRemoteDesktopAdmin
    state: present

- name: Windows | Enable Remote Desktop
  win_dsc:
    resource_name: xRemoteDesktopAdmin
    Ensure: present
    UserAuthentication: Secure

- name: Windows | Check for xNetworking Powershell module
  win_psmodule:
    name: xNetworking
    state: present

- name: Firewall | Allow RDP through Firewall
  win_dsc:
    resource_name: xFirewall
    Name: "Administrator access for RDP (TCP-In)"
    Ensure: present
    Enabled: True
    Profile: "Domain"
    Direction: "Inbound"
    Localport: "3389"
    Protocol: "TCP"
    Description: "Opens the listener port for RDP"

These steps are pretty straight forward.   We make sure the Powershell DSC module we are going to use to configure RDP is available, then use it to enable RDP.   However, since the firewall is on by default in Windows Server 2012 R2 we also need to make sure the RDP listener port is open which we used the xNetworking module to take care of.   Ansible makes working with Powershell DSC very straight forward, compare the values we're passing to the module with the ones documented for xFirewall.

Time test this updated playbook, to do that we simply re-execute the same command we entered before:
ansible-playbook domain_controller.yml -i environments/test/hosts
Now with this execution, Ansible will detect that Powershell 5.0 is already installed and since nothing changed in that step we will skip the reboot.   It will then go on to make the necessary adjustments to install the missing PowerShell modules and enable RDP.   This is an example of Ansible's typically idempotent operation.  An operation is idempotent if the result of performing it once is exactly the same as the result of performing it repeatedly without any intervening actions.   This style of execution allows us to continue iterating on our roles and repeatedly execute them on the same hosts without ill effects.
Try executing the playbook again, you should see all steps coming back as OK or Skipped indicating no changes needed to be made.
The final step we need to add is to rename the host to match our inventory.  Currently, all of our VMs have the name assigned to them by the vagrant template.  Add these steps to the end of the file:
# Make sure our hostname matches the inventory and isn't still the value from vagrant template
- name: Change hostname to match inventory
  win_domain_membership:
    hostname: ""
    domain_admin_user: ""
    domain_admin_password: ""
    workgroup_name: WORKGROUP
    state: workgroup
  register: name_state

- name: Reboot to complete name change
  when: name_state.changed
  win_reboot:
    shutdown_timeout: 600
    reboot_timeout: 600
    post_reboot_delay: 120
But, this step would be problematic if the host had joined the domain (or was a domain controller) so we need to verify that we're in a workgroup and need to complete this step.  Add the following line to the win_domain_membership task:
  when: ansible_env.COMPUTERNAME != inventory_hostname|upper
Execute the playbook one more time to rename the host.   Now that we have a fleshed out a set of common tasks, let's move on Part 3 - Creating our domain controller and member server roles.