Planet PowerShell logo

Contents

Spinning Up Lab Vms With Vagrant on Hyper-V and Provisioning Them With Ansible

I have lately been playing around with deploying virtual machines for lab purposes and for that I have been using Hyper-V on my local Windows PC. For configuring the VMs both for Linux and for Windows I have been using Ansible. But I quickly got tired of manually spinning up Virtual Machines, or copying from templates, which made me start looking into Vagrant.

Vagrant is a framework that is used for automating deployments of virtual machines on different providers such as VMware, Hyper-V, VirtualBox, and many others. The great thing about vagrant is that everything is configured in a declarative config file and you can already find VM template (or “boxes” as it is called in vagrant), for pretty much any workload you want to spin up.

You could install and use Vagrant directly on Windows and deploy to Hyper-V with no problem. Still, if you want to combine Vagrant with Ansible for complete provisioning and configuring your VMs, then you will need to use WSL. Running Vagrant together with Ansible in WSL, but provisioning to Hyper-V running on your Windows host requires some configurations. But I have done pretty much all the hard work and will guide you through how you can get it up and running.

Prerequisites

Step 1 - Installing Vagrant on WSL and Windows

To install Vagrant in WSL, you will need to open your WSL instance as an administrator. On my machine, I just open Windows Terminal as administrator, and then open my Ubuntu instance.

Then you need to go to the Vagrant releases page and download the latest release. At the time of writing this post, I will be using version 2.2.19. Vagrant Versions | HashiCorp Releases

On the web page, find the latest version and click on it. Then you will need to download the file named: x_86_64.dep, right-click on it, and copy the link address.

Then go to your WSL terminal and run the following command, with the link you just copied.

1
wget https://releases.hashicorp.com/vagrant/2.2.19/vagrant_2.2.19_x86_64.deb

The to install the debian package run the command:

1
sudo apt install ./vagrant_2.2.19_x86_64.deb

Then once the package is finished downloading on Ubuntu, you will need to install it on Windows as well. The important thing here is that it needs to be the exact same version as on WSL.

Go to the vagrant releases page again, and the same version, but now download the file named: 86_64.msi, to your windows machine. Once downloaded run the msi installer, and install the application.

Step 2 - Installing Ansible on WSL

Once you have Vagrant installed you will need to install Ansible. Ansible only runs on Linux, this is the reason why you will need the WSL instance for provisioning Vagrant machines with Ansible.

To install Ansible run the following command on your WSL instance.

1
sudo apt install ansible -y

Then once installed you will need to enable two community modules, the first module is for running docker configurations, and the second module is for provisioning Windows machines.

1
2
ansible-galaxy collection install community.docker
ansible-galaxy collection install ansible.windows

Step 3 - Configuring WSL for Vagrant and Ansible

Now we need to do some important configurations on your WSL instance for this to work properly. We will need to set some environment variables for Vagrant to be able to communicate with the Windows host’s Hyper-V. We will need to install some python packages for Ansible to work. And then we will need to configure WSL to be able to set Linux folder permissions on windows folders. I will explain in depth why and how we do this.

Setting WSL Vagrant Environment Variables

You will need to set the following environment variables on your WSL instance:


VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH

  • For vagrant to use Hyper-V as the VM provider, you will need to store the vagrant VM files on your windows host system. Usually, you would create a folder named “vagrant” inside your user home folder in Windows. Then you will need to set this environment variable to point to that specific folder. An important note here is that the virtual machines created and hard drives created will be stored inside this folder you point to.

Example:

  • VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH="/mnt/c/Users/chris/vagrant"

VAGRANT_WSL_ENABLE_WINDOWS_ACCESS

  • This setting is used to allow vagrant on your WSL instance to deploy Virtual Machine to your Windows installation on Hyper-V"

Example:

  • VAGRANT_WSL_ENABLE_WINDOWS_ACCESS=“1”

VAGRANT_DEFAULT_PROVIDER

  • This setting is used to specify that the virtual machine provider is Hyper-V

Example:

  • VAGRANT_DEFAULT_PROVIDER=“hyperv”

To set the variables you can open the bashrc file, using Nano. You do this by typing

1
nano ~/.bashrc

Then go to the bottom of the file and add the following lines:

1
2
3
export VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH="/mnt/v/vagrant"
export VAGRANT_WSL_ENABLE_WINDOWS_ACCESS="1"
export VAGRANT_DEFAULT_PROVIDER="hyperv"

If you are using zsh, then you will need to add the variables to you ~/.zshrc file.

Installing the Python packages for Ansible

The packages you will need to install are used for ansible to do docker configurations on a VM, and to be able to configure a Windows VM, using WINRM.

run the commands:

1
2
3
4
5
sudo apt install python3-pip
pip3 install docker
pip3 install docker-py
pip3 install docker-compose
pip3 install pywinrm --user

Configuring WSL to allow setting linux permissions on Windows files

You will need to enable WSL to be able to change file permissions on files located on your Windows host. If you don’t set this setting then you will not be able to ssh into Linux VMs or use ansible on your Linux VMs, because the SSH keys created by Vagrant will not have the correct permissions to be used.

On your WSL box edit the file /etc/wsl.conf by running the command:

1
sudo nano /etc/wsl.conf

add the following to the file

1
2
[automount]
options = "metadata"

Disable Ansible host key checking

Then you will need to disable host key checking. This setting is required for you to be able to provision Linux machines with Ansible using Vagrant.

edit the file: /etc/ansible/ansible.cfg, and uncomment the following line:

1
host_key_checking = False

Step 4 - Deploying A Virtual Machine to Hyper-V

So for this example, I will deploy a Windows Server 2022 Standard Core, and configure the server to be a primary domain controller in a new Forrest.

Now to do this I will need two files. A Vagrant file, which is used for declaring how the Virtual Machine should be created. And a playbook.yml file which is used by Ansible to provision the server as a domain controller.

Now I won’t go too in-depth on how everything works. If you really want to learn more about Vagrant I suggest you go to their docs: Get Started | Vagrant - HashiCorp Learn. And if you want to learn more about ansible you can read more on their docs: Getting started with Ansible — Ansible Documentation. Both are great sites for learning the tools in depth.

For this post you can find all the code on my github repo: BlogContent/IaC/vagrant/win_dc_with_ansible at main · ScriptingChris/BlogContent (github.com)

Both the Vagrantfile and the playbook.yml file should be located inside a new folder in the vagrant folder on your windows system: “C:\Users\chris\vagrant\lab-dc01"

Creating the Vagrantfile

To create the Vagrant file we start by defining the complete vagrant configuration

1
2
3
4
Vagrant.configure("2") do |config|


end

Then inside this configuration, we can then define our virtual machine with the name “lab-dc01”. Now for the VM, I want to use the template vagrant box “gusztavvargadr/windows-server-2022-standard-core”. you can find all kinds of boxes for vagrant on your site: Discover Vagrant Boxes - Vagrant Cloud (vagrantup.com). I then set the VM to use my virtual switch “External Switch”, which I have created inside Hyper-V, and given the name ‘External Switch’. I then set the provider to be Hyper-V. I define the hostname of the machine to be: “lab-dc01”. And I specify that I do not want to synchronize any folder to the virtual machine, from my WSL instance.

1
2
3
4
5
6
7
8
9
Vagrant.configure("2") do |config|
	config.vm.define "lab-dc01", primary: true do |lab-dc01|
		dc01.vm.box = "gusztavvargadr/windows-server-2022-standard-core"
		dc01.vm.network "public_network", bridge: "External Switch"
		dc01.vm.provider "hyperv"
		dc01.vm.hostname = "lab-dc01"
		dc01.vm.synced_folder '.', '/vagrant', disabled: true
	end
end

Now I can specify exactly how the virtual hardware should be created on the VM. This part is pretty self-explanatory, and if you want to dig into each setting the Vagrant docs are a great place to check it out: Hyper-V Provider | Vagrant by HashiCorp (vagrantup.com).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dc01.vm.provider "hyperv" do |h|
	h.enable_virtualization_extensions = false
	h.linked_clone = false
	h.cpus = 2
	h.memory = 4096
	h.vmname = "lab-dc01"
	h.auto_stop_action = "ShutDown" # (ShutDown, TurnOff, Save)
	h.auto_start_action = "Nothing" # (Nothing, StartIfRunning, Start)
	# h.vlan_id = "100"
end

One important note here is that for hyper-v you should have the linked_clone set to false. And if you are using an AMD CPU you will need to set the setting enable_virtualization_extensions to false as well.

The last thing you need to define in the Vagrant file is the provisioning by ansible. Here you just point Vagrant to use the Ansible playbook you have created.

1
2
3
4
lab-dc01.vm.provision "ansible" do |ansible|
	ansible.verbose = "v"
	ansible.playbook = "playbook.yml"
end

The complete Vagrantfile should look similar to below:

Vagrant.configure("2") do |config|

    config.vm.define "lab-dc01", primary: true do |dc01|
        dc01.vm.box = "gusztavvargadr/windows-server-2022-standard-core" #"gusztavvargadr/windows-server"
        dc01.vm.network "public_network", bridge: "External Switch"
        dc01.vm.provider "hyperv"
        dc01.vm.hostname = "lab-dc01"
        dc01.vm.synced_folder '.', '/vagrant', disabled: true

        dc01.vm.provider "hyperv" do |h|
            h.enable_virtualization_extensions = false
            h.linked_clone = false
            h.cpus = 2
            h.memory = 4096
            h.vmname = "lab-dc01"
            h.auto_stop_action = "ShutDown" # (ShutDown, TurnOff, Save)
            h.auto_start_action = "Nothing" # (Nothing, StartIfRunning, Start)
            # h.vlan_id = "100"
        end

        dc01.vm.provision "ansible" do |ansible|
            ansible.verbose = "v"
            ansible.playbook = "playbook.yml"
        end
    end
end

Creating the Ansible playbook

I won’t go in-depth on everything the playbook does, but basically, it starts by defining some variables. Here you will be able to define the IP address of the DC, the name of the DC, then the name of the Domain, and so on.

The playbook will then go through the steps:

  • Setting a static IP
  • Setting the password for the domain administrator
  • setting the upstream DNS servers
  • setting the NTP servers (They are set for Denmark, you might want to change them for your own country)
  • Installing the Active Directory Domain Services role
  • Creating the Domain Forrest
  • Setting the DNS for the DC to itself
  • Creating a reverse DNS Zone for my network

The complete playbook:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
---
- name: Configure Windows Server DC
  hosts: all
  vars:
    dc_address: 192.168.20.130
    dc_netmask_cidr: 24
    dc_gateway: 192.168.20.1
    dc_hostname: 'lab-dc01'
    domain_name: "lab.local"
    local_admin: 'vagrant'
    temp_password: 'vagrant'
    dc_password: '[email protected]'
    recovery_password: '[email protected]'
    upstream_dns_1: 1.1.1.1
    upstream_dns_2: 1.0.0.1
    reverse_dns_zone: "192.168.20.0/24"
    ntp_servers: "0.dk.pool.ntp.org,1.dk.pool.ntp.org"
  gather_facts: false
  tasks:

  - name: Set static IP address
    win_shell: "Get-NetIpAddress -InterfaceAlias 'Ethernet' | New-NetIPAddress -IpAddress {{ dc_address }} -PrefixLength {{ dc_netmask_cidr }} -DefaultGateway {{ dc_gateway }}" 
    ignore_errors: True
  
  - name: Add host to Ansible inventory with new IP
    add_host:
      name: '{{ dc_address }}'
      ansible_user: '{{ local_admin }}'
      ansible_password: '{{ temp_password }}'
      ansible_connection: winrm
      ansible_winrm_server_cert_validation: ignore
      ansible_winrm_port: 5985
      ansible_winrm_schema: http
  - name: Wait for system to become reachable over WinRM
    wait_for_connection:
      timeout: 120
    delegate_to: '{{ dc_address }}'

  - name: Setting local admin password
    win_user:
      name: administrator
      password: "{{dc_password}}"
      state: present
    delegate_to: '{{ dc_address }}'
    ignore_errors: True

  - name: Set upstream DNS server 
    win_dns_client:
      adapter_names: '*'
      ipv4_addresses:
      - '{{ upstream_dns_1 }}'
      - '{{ upstream_dns_2 }}'
    delegate_to: '{{ dc_address }}'

  - name: Stop the time service
    win_service:
      name: w32time
      state: stopped
    delegate_to: '{{ dc_address }}'

  - name: Setting the NTP servers
    win_shell: 'w32tm /config /syncfromflags:manual /manualpeerlist:"{{ntp_servers}}"'
    delegate_to: '{{ dc_address }}'  
  
  - name: Start the time service
    win_service:
      name: w32time
      state: started  
    delegate_to: '{{ dc_address }}'

  - name: Change the hostname of dc
    win_hostname:
      name: '{{ dc_hostname }}'
    register: res
    delegate_to: '{{ dc_address }}'
  
  - name: Reboot
    win_reboot:
      reboot_timeout: 60
    when: res.reboot_required   
    delegate_to: '{{ dc_address }}'

  - name: Install Active Directory Role
    win_feature: >
        name=AD-Domain-Services
        include_management_tools=yes
        include_sub_features=yes
        state=present        
    register: result
    delegate_to: '{{ dc_address }}'
  
  - name: Create Domain
    win_domain: >
      dns_domain_name='{{ domain_name }}'
      safe_mode_password='{{ recovery_password }}'      
    register: ad
    delegate_to: "{{ dc_address }}"

  - name: reboot server
    win_reboot:
      msg: "Installing AD. Rebooting..."
      reboot_timeout: 300
      pre_reboot_delay: 15
    when: ad.changed
    delegate_to: "{{ dc_address }}"
  
  - name: Set internal DNS server 
    win_dns_client:
      adapter_names: '*'
      ipv4_addresses:
      - '127.0.0.1'
    delegate_to: '{{ dc_address }}'

  - name: Create reverse DNS zone
    win_shell: "Add-DnsServerPrimaryZone -NetworkID {{reverse_dns_zone}} -ReplicationScope Forest"
    delegate_to: "{{ dc_address }}"    
    retries: 30
    delay: 60
    register: result           
    until: result is succeeded

Creating the Virtual Machine

Now once you have the two files ready inside your home folder C:\Users\chris\vagrant\lab-dc01. You are ready to deploy. To do this you will need to open your WSL instance and navigate to the vagrant folder inside your Windows home folder. The path for this will look similar to: “/mnt/c/Users/chris/vagrant/lab-dc01/”. Then inside this folder, you can now run the command:

1
vagrant up

This will start deploying the virtual machine. Now the first time you are deploying a new box, it will take extra time to download the box first. For a Windows box on my network, it takes between 8-10 minutes to download a box, and 4-6 minutes to download a Linux box.

/images/spinning-up-lab-vms-with-vagrant-on-hyperV-and-provisioning-them-with-ansible/provision-windows-with-vagrant-and-ansible_1.png

Once the box is downloaded and the machine is deployed, you should see the Ansible provisioning starting. Now for this playbook, it will output some errors. These errors are for when the machine Ip address changes and it loses the winrm connection for a short period of time.

If the Vagrant up command fails with the following error:

/images/spinning-up-lab-vms-with-vagrant-on-hyperV-and-provisioning-them-with-ansible/provision-windows-with-vagrant-and-ansible_2.png

I think it is a vagrant bug which I have experienced a couple of times, where it loses the network connection to the VM, and cant create the winrm connection for Ansible. You can run the following command, to continue the provisioning.

1
vagrant provision

This should start the Ansible provisioning.

On the image below you see the IP address change error the playbook might produce. Again the playbook will continue and you can ignore the error:

/images/spinning-up-lab-vms-with-vagrant-on-hyperV-and-provisioning-them-with-ansible/provision-windows-with-vagrant-and-ansible_3.png

Checking that everything worked.

Now after the ansible-playbook is finished provisioning the machine you can check that it actually worked by connecting to the VM.

If you open a PowerShell terminal and type in the following command:

1
Enter-PSSession -VMName lab-dc01 -Credential (Get-Credential)

and then provide the credential from the variables inside the playbook.yml file. In my case it would be: Username: [email protected] Password: [email protected]

This should grant you a PowerShell terminal on the Virtual Machine. Now run the following command, to check that the domain has been created:

1
Get-ADDomain

You should get an output similar to below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
AllowedDNSSuffixes                 : {}
ChildDomains                       : {}
ComputersContainer                 : CN=Computers,DC=lab,DC=local
DeletedObjectsContainer            : CN=Deleted Objects,DC=lab,DC=local
DistinguishedName                  : DC=lab,DC=local
DNSRoot                            : lab.local
DomainControllersContainer         : OU=Domain Controllers,DC=lab,DC=local
DomainMode                         : Windows2016Domain
DomainSID                          : S-1-5-21-4197132403-3289389793-3957751674
ForeignSecurityPrincipalsContainer : CN=ForeignSecurityPrincipals,DC=lab,DC=local
Forest                             : lab.local
InfrastructureMaster               : lab-dc01.lab.local
LastLogonReplicationInterval       :
LinkedGroupPolicyObjects           : {CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=lab,DC=local}
LostAndFoundContainer              : CN=LostAndFound,DC=lab,DC=local
ManagedBy                          :
Name                               : lab
NetBIOSName                        : LAB
ObjectClass                        : domainDNS
ObjectGUID                         : 25084423-12b3-445e-ac43-5b11aecd2235
ParentDomain                       :
PDCEmulator                        : lab-dc01.lab.local
PublicKeyRequiredPasswordRolling   : True
QuotasContainer                    : CN=NTDS Quotas,DC=lab,DC=local
ReadOnlyReplicaDirectoryServers    : {}
ReplicaDirectoryServers            : {lab-dc01.lab.local}
RIDMaster                          : lab-dc01.lab.local
SubordinateReferences              : {DC=ForestDnsZones,DC=lab,DC=local, DC=DomainDnsZones,DC=lab,DC=local,
                                     CN=Configuration,DC=lab,DC=local}
SystemsContainer                   : CN=System,DC=lab,DC=local
UsersContainer                     : CN=Users,DC=lab,DC=local

Conclusion

Vagrant is a pretty awesome tool for spinning up virtual machines on your Hyper-V host. The great thing about it is that you can combine it with Ansible for complete VM provisioning. This way you can spin up an entire Windows Domain lab in just a matter of minutes. I will definitely be using this tool, for a lot of my labs going forward. I will make sure to post all my Ansible and Vagrant Template configs on my Github Templates Repo: ScriptingChris/Templates: Templates for all kinds of technologies (Github.com).