Automated build of two Rocky Linux 10 VMs on Hyper-V using PowerShell (VM provisioning) + Ansible (configuration management).
┌─────────────────────────────────────────────────────────────────┐
│ Windows Hyper-V Host │
│ │
│ ┌──────────────────────────┐ ┌───────────────────────────┐ │
│ │ lab-devtools │ │ lab-k8s │ │
│ │ 192.168.100.10 │ │ 192.168.100.20 │ │
│ │ │ │ │ │
│ │ • Git │ │ • RKE2 (single-node) │ │
│ │ • Docker CE │ │ • Single-node control │ │
│ │ • Docker Compose │ │ plane + worker │ │
│ │ • Helm │ │ • Rancher Manager 2.13 │ │
│ │ • kubectl │ │ • cert-manager │ │
│ │ • k9s │ │ • ArgoCD │ │
│ │ • Docker Registry :5000 │ │ │ │
│ │ • Pi-hole DNS :53/:8080 │ │ │ │
│ └──────────┬───────────────┘ └──────────────┬────────────┘ │
│ └──────────────────────────────────┘ │
│ LabSwitch (192.168.100.0/24) │
│ NAT → Internet │
└─────────────────────────────────────────────────────────────────┘
- Windows 10/11 or Windows Server with Hyper-V enabled
- Rocky Linux 10 minimal ISO downloaded:
C:\ISOs\Rocky-10.1-x86_64-minimal.iso- Download: https://rockylinux.org/download
- Ansible available on WSL2 or a separate Linux machine
- PowerShell 7+ (recommended) or Windows PowerShell 5.1
├── powershell/
│ ├── New-LabVMs.ps1 # Creates Hyper-V VMs + NAT switch + kickstart disks
│ └── Setup-AnsibleSSH.ps1 # SSH key setup for Ansible auth
├── ansible.cfg
├── site.yml # Main playbook
├── requirements.yml # Ansible Galaxy collections
├── hosts.yml # Inventory
├── group_vars/
│ └── all.yml # Versions, IPs, passwords
└── roles/
├── common/ # Baseline: SELinux, sysctl, packages, /etc/hosts
├── devtools/ # Git, Docker, Helm, kubectl, k9s, argocd, kubeseal
├── registry/ # Private Docker registry (port 5000)
├── pihole/ # Pi-hole DNS server (port 53 / web UI 8080)
├── rke2/ # RKE2 server install + kubeconfig
├── rancher/ # cert-manager + Rancher Manager
└── argocd/ # ArgoCD GitOps engine
# Adjust ISOPath if your ISO is elsewhere
.\powershell\New-LabVMs.ps1 -ISOPath "C:\ISOs\Rocky-10.1-x86_64-minimal.iso"
# Ansible user password: Admin1234!This creates:
- Hyper-V internal switch
LabSwitchwith NAT (192.168.100.0/24) lab-devtools— 4 vCPU / 8 GB RAM / 80 GB VHDXlab-k8s— 8 vCPU / 32 GB RAM / 120 GB VHDX- Both VMs boot the Rocky Linux 10 minimal ISO with an attached OEMDRV kickstart disk
- Windows hosts file entries for
lab-devtoolsandlab-k8sare added automatically
Boot each VM in Hyper-V and start the unattended kickstart install:
For BOTH VMs:
- At the boot menu, select
anaconda kickstart - The kickstart automatically configures:
- Minimal Rocky Linux 10 install
- Static lab NIC on
eth0lab-devtools:192.168.100.10/24, GW192.168.100.1, DNS8.8.8.8lab-k8s:192.168.100.20/24, GW192.168.100.1, DNS8.8.8.8
- DHCP internet NIC on
eth1via Hyper-VDefault Switch ansibleuser with passwordless sudosshdenabled at boot
- After the first reboot, verify you can log in:
ssh ansible@192.168.100.10 ssh ansible@192.168.100.20
.\powershell\Setup-AnsibleSSH.ps1This generates ~/.ssh/lab_rsa and installs the public key on both VMs.
# Change into your clone of this repo
cd <path-to-rke2-lab-public>
# If the SSH key was created on Windows, copy it into WSL
cp "<path-to-windows-home>/.ssh/lab_rsa" ~/.ssh/lab_rsa
chmod 600 ~/.ssh/lab_rsa
# Point Ansible at the repo config when running from a Windows-mounted path
export ANSIBLE_CONFIG="$PWD/ansible.cfg"
# Install Ansible if needed
pip install ansible
# Install required Ansible Galaxy collections
ansible-galaxy collection install -r requirements.yml
# Test connectivity
ansible all -m ping
# Run the full playbook
ansible-playbook site.ymlTotal runtime: approximately 20–30 minutes (mostly RKE2 + Rancher + ArgoCD startup).
rancher.lab is published through Pi-hole DNS for the lab network and is also added to /etc/hosts on the lab VMs by Ansible.
Open: https://rancher.lab
Bootstrap password: Admin1234! (change in group_vars/all.yml before deploying!)
Open: http://argocd.lab
| Field | Value |
|---|---|
| Username | admin |
| Password | Printed at the end of the Ansible run — or retrieve manually: |
kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml \
get secret argocd-initial-admin-secret -n argocd \
-o jsonpath='{.data.password}' | base64 -dChange the password after first login via User Info → Update Password, then delete the
argocd-initial-admin-secretsecret.
The public repo does not create any extra ArgoCD local users by default. If you want a readonly account for a dashboard or widget integration, enable it in group_vars/all.yml:
argocd_create_local_readonly_user: true
argocd_local_readonly_user_name: homepageAfter deployment, set a password for that user:
argocd login argocd.lab --username admin --insecure
argocd account update-password \
--account <readonly-user> \
--current-password "<current password if already set>" \
--new-password "<new password>"Generate a token after the password is set:
argocd login argocd.lab --username <readonly-user> --insecure
argocd account generate-token --account <readonly-user>Use that token in your chosen integration.
Open: http://192.168.100.10:8080/admin
Password: Admin1234! (change pihole_web_password in group_vars/all.yml)
Pi-hole is automatically configured as the DNS server for both lab VMs. To use it from Windows or WSL and get full *.lab resolution without a hosts file:
Windows: Control Panel → Network adapter → IPv4 → set Preferred DNS to 192.168.100.10
WSL2:
# /etc/resolv.conf (prevent WSL from overwriting it first)
echo '[network]' | sudo tee /etc/wsl.conf
echo 'generateResolvConf = false' | sudo tee -a /etc/wsl.conf
sudo tee /etc/resolv.conf <<EOF
nameserver 192.168.100.10
nameserver 1.1.1.1
search lab
EOFPi-hole resolves these lab names out of the box:
| Hostname | IP |
|---|---|
lab-devtools / lab-devtools.lab |
192.168.100.10 |
lab-k8s / lab-k8s.lab |
192.168.100.20 |
rancher.lab |
192.168.100.20 |
argocd.lab |
192.168.100.20 |
Add new Pi-hole DNS names in
group_vars/all.ymlunderpihole_extra_dns_records, then re-run--tags pihole.Extra host aliases for the lab VMs can be added under
lab_additional_host_aliases. Optional dnsmasq rules such as wildcard app domains can be added underpihole_extra_dnsmasq_lines.
From any machine that can reach 192.168.100.10:
# Tag and push an image
docker tag myimage:latest 192.168.100.10:5000/myimage:latest
docker push 192.168.100.10:5000/myimage:latest
# Pull it
docker pull 192.168.100.10:5000/myimage:latest
# List all images in the registry
curl http://192.168.100.10:5000/v2/_catalogOn the k8s node, Kubernetes is pre-configured to mirror pulls through the registry.
# Only configure devtools
ansible-playbook site.yml --tags devtools,registry
# Only deploy Pi-hole (and reconfigure DNS on all hosts)
ansible-playbook site.yml --tags pihole
# Only deploy RKE2 (skip Rancher + ArgoCD)
ansible-playbook site.yml --tags rke2
# Re-run just Rancher
ansible-playbook site.yml --tags rancher
# Re-run just ArgoCD
ansible-playbook site.yml --tags argocd
# Re-run just the registry setup
ansible-playbook site.yml --tags registryAll versions and IPs are in group_vars/all.yml:
| Variable | Default | Description |
|---|---|---|
rke2_version |
v1.34.5+rke2r1 | RKE2 / Kubernetes version |
rancher_version |
2.13.3 | Rancher Manager version |
cert_manager_version |
v1.19.4 | cert-manager version |
helm_version |
v4.1.3 | Helm version |
kubectl_version |
v1.34.5 | kubectl version |
k9s_version |
v0.50.18 | k9s version on lab-devtools |
argocd_cli_version |
v2.14.20 | ArgoCD CLI version on lab-devtools |
kubeseal_version |
0.28.0 | kubeseal version on lab-devtools |
rancher_hostname |
rancher.lab | Rancher ingress hostname |
rancher_bootstrap_password |
Admin1234! | Change this! |
argocd_chart_version |
7.7.3 | ArgoCD Helm chart version |
argocd_hostname |
argocd.lab | ArgoCD ingress hostname |
argocd_create_local_readonly_user |
false | Create an optional readonly local ArgoCD user |
argocd_local_readonly_user_name |
homepage | Username for the optional readonly ArgoCD user |
pihole_version |
2025.03.0 | Pi-hole container tag |
pihole_web_port |
8080 | Pi-hole web UI port |
pihole_web_password |
Admin1234! | Change this! |
lab_additional_host_aliases |
[] | Extra hostnames added to /etc/hosts on lab VMs |
pihole_extra_dns_records |
{} | Extra Pi-hole DNS records for your own services |
pihole_extra_dnsmasq_lines |
[] | Optional Pi-hole dnsmasq directives such as wildcard app domains |
private_registry_port |
5000 | Registry listen port |
RKE2 won't start:
sudo journalctl -u rke2-server -f
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get pods -ARancher pods crashing:
kubectl get pods -n cattle-system
kubectl describe pod -n cattle-system <pod-name>ArgoCD not reachable:
kubectl get pods -n argocd
kubectl get ingress -n argocd
kubectl describe pod -n argocd -l app.kubernetes.io/name=argocd-serverPi-hole not resolving / port 53 in use:
# On lab-devtools
sudo systemctl status lab-pihole
sudo docker compose -f /opt/pihole/docker-compose.yml logs
# Test DNS resolution
dig @192.168.100.10 rancher.labRegistry unreachable:
# On lab-devtools
sudo docker compose -f /opt/registry/docker-compose.yml ps
sudo systemctl status lab-registry
curl http://localhost:5000/v2/Ansible can't connect:
ssh -i ~/.ssh/lab_rsa ansible@192.168.100.10
ansible all -m ping -vvv