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:
- Configure NTP
- Change hostname to match DNS entry
- Change IP to match DNS entry
- Remove Old IP from server
- Install the ADDS Role
- 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.