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.
- 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.