Skip to content

Automate Windows VM Creation and Configuration in vSphere Using Packer, Terraform and Ansible (Part 3 of 3)

feature

This is the final entry in the series. In this post, I want to show how Ansible can be used to automate Windows VM provisioning. As always all the scripts and configurations are available at my GitHub repository.

Provisioning with Ansible

Actually, Ansible was not my first choice when it came to VM provisioning. I've spent a lot of time with Chef at first because it was used in this repo which I took as a starting point. It almost worked for me but I've encountered a problem with a domain joining process. In order to join a domain the DNS server setting of the joining machine should be pointing to the domain controller. When the DNS server setting changed, the Chef client on that machine stopped resolving the Chef server and was unable to continue operation. Of course, I could fix it by adding necessary DNS entry on the server beforehand. Instead, I decided to try out Ansible which cannot run into such problems due to its agentless design.

Setting up Ansible

  • Install Ansible following the official installation guide
  • Install pywinrm library by issuing pip install pywinrm. Ansible uses this library to connect to Windows machines.
  • Clone my Github repo and cd to ansible
  • Edit inventory.yml and group_vars/all.yml according to your environment

I use Ansible Vault to store my credentials in group_vars/all.yml in encrypted form. To create your own encrypted passwords issue

ansible-vault encrypt_string <string_to_encrypt>
command for each password you want to encrypt. It will ask you for the new vault password. Put that password in .vault_pass file in Ansible working directory.

Configuration files

Ansible directory structure:

.
├── group_vars
   └── all.yml
├── roles
   ├── common
      └── tasks
          ├── enable_rdp.yml
          └── main.yml
   ├── fileserver
      └── tasks
          └── main.yml
   ├── primary_domain_controller
      └── tasks
          └── main.yml
   └── replica_domain_controller
       └── tasks
           └── main.yml
├── .vault_pass
├── ansible.cfg
├── inventory.yml
└── winlab.yml

Executing playbooks

When you execute ansible-playbook winlab.yml Ansible reads ansible.cfg which points to inventory file and vault password file.

[defaults]
inventory = inventory.yml
vault_password_file = ./.vault_pass
Then it starts to apply roles from winlab.yml

---
- hosts: primary_domain_controller
  roles:
    - primary_domain_controller
    - common

- hosts: replica_domain_controller
  roles:
    - replica_domain_controller
    - common

- hosts: fileserver
  roles:
    - fileserver
    - common
to hosts in inventory.yml.

---
primary_domain_controller:
  hosts:
    10.5.202.4:
replica_domain_controller:
  hosts:
    10.5.202.5:
fileserver:
  hosts:
    10.5.202.6:

Primary domain controller (PDC) is configured by roles/primary_domain_controller/tasks/main.yml.

---
- name: install ad
  win_domain:
    dns_domain_name: "{{ domain }}"
    domain_netbios_name: "{{ netbios_domain }}"
    safe_mode_password: "{{ domain_safemode_password }}"
  register: ad

- name: reboot server
  win_reboot:
    msg: "Installing AD. Rebooting..."
    pre_reboot_delay: 15
    reboot_timeout: 600
    post_reboot_delay: 420
  when: ad.changed

The install ad task installs the AD DS role on the server, creates a new forest and promotes the server to a domain controller. The reboot server reboots the server only if the status of the previous task returned "changed".

win_reboot module doesn't have any reliable way to tell if the system is ready for management after the reboot. When Windows is rebooted after becoming a domain controller it takes a substantial amount of time to finish all the related tasks. To address this issue I specify the post_reboot_delay parameter. You may have to adjust it depending on your system's performance. Please refer to official module documentation for further details.

RDP is enabled on PDC by roles/common/tasks/main.yml which calls roles/common/tasks/enable_rdp.yml.

This task is applied to all machines, so it is omitted hereafter.

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

This one installs xRemoteDesktopAdmin PowerShell module with win_psmodule and enables RDP using PowerShell Desired State Configuration. Then xNetworking module is installed to open RDP port on the Windows Firewall with win_dsc again.

Replica domain controller (RDC) is configured by roles/replica_domain_controller/tasks/main.ymlscript.

---
- name: change DNS server
  when: not ansible_windows_domain_member
  win_dns_client:
    adapter_names: '*'
    ipv4_addresses: "{{ groups['primary_domain_controller'][0] }}"

- name: join domain
  win_domain_membership:
    dns_domain_name: "{{ domain }}"
    domain_admin_user: "{{ domain_admin }}"
    domain_admin_password: "{{ domain_admin_password }}"
    state: domain
  register: domain_joined

- name: reboot after domain join
  win_reboot:
  when: domain_joined.reboot_required

- name: Wait for system to become reachable over WinRM
  wait_for_connection:
    timeout: 900

- name: install ad
  win_domain_controller:
    dns_domain_name: "{{ domain }}"
    domain_admin_user: "{{ domain_admin }}"
    domain_admin_password: "{{ domain_admin_password }}"
    safe_mode_password: "{{ domain_safemode_password }}"
    state: domain_controller
  register: ad

- name: reboot server
  win_reboot:
    msg: "Installing AD. Rebooting..."
    pre_reboot_delay: 15
  when: ad.changed

First DNS server is changed to point to the PDC. Then the server joins the domain and reboots. After that AD role is installed and server reboots.

The file server is configured by roles/fileserver/tasks/main.yml.

---
- name: change DNS server
  win_dns_client:
    adapter_names: '*'
    ipv4_addresses: 
      - "{{ groups['primary_domain_controller'][0] }}"
      - "{{ groups['replica_domain_controller'][0] }}"

- name: join domain
  win_domain_membership:
    dns_domain_name: "{{ domain }}"
    domain_admin_user: "{{ domain_admin }}"
    domain_admin_password: "{{ domain_admin_password }}"
    state: domain
  register: domain_joined

- name: reboot after domain join
  win_reboot:
  when: domain_joined.reboot_required

It repeats the steps taken with SDC except install ad task.

Here is the sample output of ansible-playbook winlab.yml:

ansible-playbook

As you can see not everything went smooth at first. I guess there were some issues with Internet connectivity when Ansible tried to install xRemoteDesktopAdmin module. Fortunately, it succeeded on the second try. And here is what I like about Ansible: it didn't try to install AD during the second run, because it was already there. This idempotency feature is very handy because it allows you to run playbooks against the same hosts over and over again and not to worry about making any changes to already configured resources.

Conclusion

Now you have a fully functional basic Windows domain setup which you can use to prepare for MCSA exams or to build custom PoC setups.

References and further reading

  1. Ansible Windows Guide
  2. Ansible & DSC
  3. Manage Windows like Linux with Ansible
  4. Configure An Ansible Testing System On Windows (Part 2)
  5. Configure An Ansible Testing System On Windows (Part 3)

Comments