Deploying Windows AD with Ansible [Part 2 - Domain Controller Deployment]

If you'd like to just see the github repo for this project, you can look here.

Now that we've provisioned our Windows Server 2022 template (which you can read about here), it's time to get to writing some ansible playbooks! This first one will deal with deploying the Windows template we made in the last post. Essentially we'll deploy the template, give it a hostname, and set an IP. I'm a big fan of utilizing roles and tasks to organize things in ansible so that's how I'm going to organize things. I'll be sure to put this project on GitHub so you can get a better idea of how things look.

Setup

First, we'll install the necessary packages and ansible-galaxy collections. We can do that with pip install proxmoxer and ansible-galaxy collection install community.general. the hosts file needs to be updated to include whatever the names of your proxmox hosts are. Here's an example of what that may look like:

[proxmox_nodes] 
node1.pve.domain 
node2.pve.domain 
node3.pve.domain

Once you've made those entries (and made the DNS record pointing to them), you should be able to access them via ansible. While you could use the root user for accessing the proxmox host, I've never been a fan of using the root user for any remote connections if it can be helped.

To put my mind at ease I setup my homelab, at least part of it, with RedHat's IdM as an LDAP server. The goal of doing that was to allow for me to use one login to manage all my servers (and workstations) for things like updating and management. I can also use that login for any service that syncs with ldap. It'll also come in handy as I start to configure my servers post joining to the ipa domain.

Tangent aside, we can create the initial playbook which will point to the role we'll use. It should look something like this:

---

- name: "Active Directory Deployment"
  hosts: localhost
  vars_files:
    - ../vaults/proxmox_vault.yml
    - ../vaults/windows_template_vault.yml
    - ../vaults/ipavault.yml
    - ../vaults/windows_domain_vault.yml
  roles:
    - ../roles/win-server-deploy

Notice that the host is actually our localhost. The proxmox_kvm module actually specifies the proxmox host later on in the task so we don't need to ssh to our proxmox host (which would require a remote root login, again not ideal) to deploy VMs. I went ahead and added all the vaults that I'd need later in this playbook.

The next step is to setup the role. Organization is important when working with ansible. Not only for yourself, but anybody else trying to read your playbooks. Here's what the skeletal structure of the role looks like:

win-server-deploy/
├── README.md
├── tasks
│   ├── ad_install.yml
│   ├── configure.yml
│   ├── deploy.yml
│   ├── dns_entry.yml
│   ├── main.yml
│   └── poweron.yml
└── vars
    └── main.yml

This breaks things into pretty modular components and makes it easy to modify, add, or remove parts of the play. If you wanted to keep every aspect of your playbook encrypted, you could simply place all variables related to the playbook inside the vault, but to help others when I commit my playbook , I'll use the non-important variables in the vars subdirectory of our role. All the tasks below will use generic names for the values needed.

The first step will be to add our DNS entry in IPA and to add our new server to our Ansible Hosts file. If you'd like you can do this manually and then not have to worry about it making two entries for the same server. I may remove it later, but it was good simple regex practice for the time being.

DNS Entry Via IPA

I use FreeIPA for DNS as well as an ldap server. If you're interested in something like but not as robust as AD then take a look at it here. Anyway, that's the anisble module I'll use. It should look something like this:

---
- name: "Make DNS Entry in IPA"
  ipa_dnsrecord: 
    ipa_host: ipa.server.domain.net
    ipa_pass: Password
    state: present
    zone_name: ad.domain.net
    record_name: addc0
    record_type: 'A'
    record_value: '10.10.10.2'

pretty straightforward stuff. Two things to take quick note of thought. First the zone name is the full zone name, so in this example ad.domain.net and just ad. The other is that you don't need to specify the user, it assumes the admin account for IPA. We should also add the fully qualified domain name of our soon to be deployed domain controller in the hosts file. This will allow us to communicate with the server via ansible.

Server Deployment From Template

Here's the deploy.yml task:

---

- name: "Deploy Windows Template from Proxmox"
  proxmox_kvm:
    api_user: user
    api_password: pass
    api_host:  node1.pve.domain.net
    clone: win_server_template
    name: addc0
    newid: 200
    node: node1
    target: node1
    storage: ssd-storage
    format: raw
    full: true
    timeout: 1000

The format was kept raw due to the proxmox best practices for Windows Server. I've had some issues with timeout so I increased the timeout value. The newid is the vm number proxmox assigns. I'd like to keep my ad environment in the 800 range.

Power On Windows Server

The next step of this playbook is to power the server on, which will look like this:

---

- name: "Power On New Windows Server"
  proxmox_kvm:
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_pass }}"
    api_host:  sam.pve.mcculley.tech
    name: "{{ vm_name }}"
    node: "{{ proxmox_host }}"
    state: started

- name: "Wait for server to come up"
  vars:
    - ansible_password: "{{ windows_template_pass }}"
  ansible.builtin.wait_for_connection:
  delegate_to: win22template.ad.mcculley.tech

Nothing really special here, so I'll just move on to the configuration

Domain Controller Install Configuration

The configuration will consist of 6 parts:

  1. Configure NTP
  2. Change hostname to match DNS entry
  3. Change IP to match DNS entry
  4. Remove Old IP from server
  5. Install the ADDS Role
  6. Create DNS Reverse Zone

Configure NTP

Per Microsoft's reccommendations, NTP should be set to an authoritative source. The docs give the two commands needed to make this change. You could make this change via the registry if you wanted, but the benefit of ansible is having it do all that for you!

- name: "Configure NTP"
  win_shell: 'w32tm.exe /config /syncfromflags:manual /manualpeerlist:131.107.13.100,0x8 /reliable:yes /update'

- name: "Update NTP Config"
  win_shell: 'w32tm.exe /config /update'

The only thing to note here is that since the system will reboot after changing the hostname, I don't worry about service restarting.

Change Hostname to Match DNS

Changing the hostname requires a reboot so I register the task before it and whenever a change occurs, the reboot occurs as well.

- name: "Change hostname of the Windows Server"
      win_hostname:
        name: addc0
      register: reboot
      
    - name: "Reboot to Complete hostname change"
      win_reboot:
      when: reboot.reboot_required  

Add New IP && Remove Old IP

    - name: "Set New IP on Windows Server" 
      ansible.windows.win_powershell:
        script: |
          New-NetIPAddress -InterfaceAlias "Ethernet Instance 0" -IPAddress 10.1.9.2 -AddressFamily IPv4 -PrefixLength 24 -Confirm:$false
            
    - name: "Remove Old IP on Windows Server" 
      ansible.windows.win_powershell:
        script: |
          Remove-NetIPAddress -InterfaceAlias "Ethernet Instance 0" -IPAddress 10.1.9.25 -AddressFamily IPv4 -PrefixLength 24 -Confirm:$false
      ignore_errors: true
  delegate_to: win22template.ad.mcculley.tech

There's not a super clean way of changing the IP so this will have to do. Using the ignore_errors argument allows for the playbook to continue. This takes a minute to timeout. There may be a way to make this better.

Install ADDS

Finally, the time has come to install our Domain Controller!

- name: "Install AD"
  vars:
    - ansible_password: "{{ windows_template_pass }}"
  block:
    - name: "Install Role"
      win_feature:
        name: AD-Domain-Services
        include_sub_features: true
        include_management_tools: true
        state: present

    - name: "Create domain"
      win_domain:
        dns_domain_name: "{{ win_domain_name }}"
        safe_mode_password: "{{ win_domain_safe_mode_pass }}"
      register: domain_install

It's crazy to me that a process that was once an annoying series of clicking and reboots can now be boiled down to the config above. Yes, we did a lot to make this install simple and it's not often you need to deploy domain controllers, but for someone looking to create an AD environment for testing and easily break it down when needed, this is great! Notice that I delegate this at the end of the block so I don't have to specify every task should be performed by the DC.

Create Reverse DNS Zone

The final step is to create a reverse DNS zone for our AD network. Once again there's not a super clean way to do this, so the ansible powershell module will have to do.

  - name: "Create Reverse DNS Zone"
      ansible.windows.win_powershell:
        script: |
          Add-DnsServerPrimaryZone -NetworkID "10.1.9.0/24" -ReplicationScope "Domain"

    - name: "reboot server"
      win_reboot:
      when: domain_install.changed 

  delegate_to: addc0.ad.mcculley.tech

You could add this after the block above or you could run it before the reboot after hostname change, it's up to you.

Conclusion

In conclusion, this playbook, which is organized into a role, uses vault files for credentials, and vars files for ease of configuration, should give you a good idea of how to automate deployment of a domain controller via ansible. I've learned a lot doing this and had a lot of fun! Next I'm planning on adding some users and OUs to my domain via ansible.

Follow Me on Mastodon! Follow Me on Twitter