Intro
I used Virtualbox to launch Linux vm using Vagrant for many years, have to switch to Hyper-V due to it by default enabled in win10 host.
The outstanding point comparing to Virtualbox is that it supports nested virtualization, means you can use kvm hypervisor on top of Hyper-V Linux VM, to launch nested vm easily.
The drawback or learning curve with Hyper-V is that network model is different than Virtualbox.
I will address few pain points and go through completely k8s lab deployment on top of Hyper-V on win10 host in this post.
- static ip for Hyper-V vm and permanent ip for k8s API server
- experiment k8s setup automation with Vagrant
Lab env
- Win10 32GB RAM
- Hyper-V
- vagrant 2.4.1 windows
- WSL2 optional as terminal
k8s hyperv tool set
We heavy use handy scripts to make k8s cluster on Hyper-V automatically, you can clone to local or download them manually,
git clone git@github.com:robertluwang/hands-on-nativecloud.git
cd ./hands-on-nativecloud/src/k8s-cri-dockerd
Hyper-V network
3 types
:
- private - vm<->vm only
- internal - vm<->vm, vm<->host, vm->Internet if NAT enabled
- external - kind of bridge, vm<->host, vm->Internet
private network
It is isolated network, vm can access each other only.
internal network
It is private network plus host communication due to virtual NIC on host.
It is similar with Host-only in Virtualbox.
It could be further as NAT to allow vm to access Internet by adding internal network in NetNat, which enabling the routing traffic to outside.
The "Default Switch" is default NAT switch, the only issue is switch ip always changed after reboot; also DNS not working well. As remedy you can manually change vm ip and DNS inside vm, to match new change on Default Switch.
The good news is that it is possible to setup own static NAT network, to have stable test lab env, which is host-only + NAT if comparing with Virtualbox.
external switch
External switch will bind with physical NIC on host, sharing same network with host to access outside, ip assigned from DHCP.
In my pc there is only Wi-Fi adaptor, so external switch will bind to Wi-Fi adaptor, it always crashes and lost all Wi-Fi Internet access during creating, have to recover by Network Reset.
Let's forget about external switch for wi-fi at all, since it will directly impact host network access.
So Internal network + NAT is best and stable option for static IP, it allow host and vm talk each other and also access to Internet.
Create own NAT switch
We need to create new Hyper-V switch for example myNAT as type of Internal either from Hyper-V Manager GUI or Powershell as admin role, then create vNIC network like 192.168.120.0/24, and enable as NAT.
You can run PowerShell term as admin on win10 host, then run below script to complete vm switch, vNIC and NAT setup.
Please change your prefer values in script:
- hyperv switch name: myNAT
- vEthernet ip: 192.168.120.1 it is gateway on win host
- NetNAT name: myNAT
- NetNAT network: 192.168.120./24
myNAT.ps1
If ("myNAT" -in (Get-VMSwitch | Select-Object -ExpandProperty Name) -eq $FALSE) {
'Creating Internal-only switch "myNAT" on Windows Hyper-V host...'
New-VMSwitch -SwitchName "myNAT" -SwitchType Internal
New-NetIPAddress -IPAddress 192.168.120.1 -PrefixLength 24 -InterfaceAlias "vEthernet (myNAT)"
New-NetNAT -Name "myNAT" -InternalIPInterfaceAddressPrefix 192.168.120.0/24
}
else {
'Internal-only switch "myNAT" for static IP configuration already exists; skipping'
}
If ("192.168.120.1" -in (Get-NetIPAddress | Select-Object -ExpandProperty IPAddress) -eq $FALSE) {
'Registering new IP address 192.168.120.1 on Windows Hyper-V host...'
New-NetIPAddress -IPAddress 192.168.120.1 -PrefixLength 24 -InterfaceAlias "vEthernet (myNAT)"
}
else {
'IP address "192.168.120.1" for static IP configuration already registered; skipping'
}
If ("192.168.120.0/24" -in (Get-NetNAT | Select-Object -ExpandProperty InternalIPInterfaceAddressPrefix) -eq $FALSE) {
'Registering new NAT adapter for 192.168.120.0/24 on Windows Hyper-V host...'
New-NetNAT -Name "myNAT" -InternalIPInterfaceAddressPrefix 192.168.120.0/24
}
else {
'"192.168.120.0/24" for static IP configuration already registered; skipping'
}
Here is sample of running result,
.\myNAT.ps1
Creating Internal-only switch named "myNAT" on Windows Hyper-V host...
Name SwitchType NetAdapterInterfaceDescription
---- ---------- ------------------------------
myNAT Internal
IPv4Address : 192.168.120.1
IPv6Address :
IPVersionSupport :
PrefixLength : 24
SubnetMask :
AddressFamily : IPv4
AddressState : Tentative
InterfaceAlias : vEthernet (myNAT)
InterfaceIndex : 109
IPAddress : 192.168.120.1
PreferredLifetime : 10675199.02:48:05.4775807
PrefixOrigin : Manual
SkipAsSource : False
Store : ActiveStore
SuffixOrigin : Manual
Type : Unicast
ValidLifetime : 10675199.02:48:05.4775807
PSComputerName :
ifIndex : 109
IPv4Address : 192.168.120.1
IPv6Address :
IPVersionSupport :
PrefixLength : 24
SubnetMask :
AddressFamily : IPv4
AddressState : Invalid
InterfaceAlias : vEthernet (myNAT)
InterfaceIndex : 109
IPAddress : 192.168.120.1
PreferredLifetime : 10675199.02:48:05.4775807
PrefixOrigin : Manual
SkipAsSource : False
Store : PersistentStore
SuffixOrigin : Manual
Type : Unicast
ValidLifetime : 10675199.02:48:05.4775807
PSComputerName :
ifIndex : 109
Caption :
Description :
ElementName :
InstanceID : myNAT;0
Active : True
ExternalIPInterfaceAddressPrefix :
IcmpQueryTimeout : 30
InternalIPInterfaceAddressPrefix : 192.168.120.0/24
InternalRoutingDomainId : {00000000-0000-0000-0000-000000000000}
Name : myNAT1
Store : Local
TcpEstablishedConnectionTimeout : 1800
TcpFilteringBehavior : AddressDependentFiltering
TcpTransientConnectionTimeout : 120
UdpFilteringBehavior : AddressDependentFiltering
UdpIdleSessionTimeout : 120
UdpInboundRefresh : False
PSComputerName :
IP address "192.168.120.1" for static IP configuration already registered; skipping
"192.168.120.0/24" for static IP configuration already registered; skipping
Also there is a handy PowerShell script to delete hyper-v switch myNAT, vNIC and associated NAT, in case tear down NAT switch or reset it.
myNAT-delete.ps1
If (Get-VMSwitch | ? Name -Eq myNAT) {
'Deleting Internal-only switch named myNAT on Windows Hyper-V host...'
Remove-VMSwitch -Name myNAT
}
else {
'VMSwitch "myNAT" not exists; skipping'
}
If (Get-NetIPAddress | ? IPAddress -Eq "192.168.120.1") {
'Deleting IP address 192.168.120.1 on Windows Hyper-V host...'
Remove-NetIPAddress -IPAddress "192.168.120.1"
}
else {
'IP address "192.168.120.1" not existing; skipping'
}
If (Get-NetNAT | ? Name -Eq myNAT) {
'Deleting NAT adapter myNAT on Windows Hyper-V host...'
Remove-NetNAT -Name myNAT
}
else {
'NAT adapter myNAT not existing; skipping'
}
Bring up static host-only ip with Vagrant
Until now the Vagrant not supports well with Hyper-V network configuration like Virtualbox in Vagrantfile.
https://developer.hashicorp.com/vagrant/docs/providers/hyperv/limitations
host-only NIC for Virtualbox,
vm.network :private_network, ip: "192.168.99.30"
then what should we do in hyper-v Vagrantfile? we only can choice network switcher, not chance to assign static ip.
Good news is that Vagrant can ssh to new launched vm via ipv6 IP, means we have chance to run any provision script, so as remedy we can run a shell inline or offline in provision stage, to setup static ip to eth0 manually, also correct /etc/resolv.conf for DNS setting.
Here is sample to launch Ubuntu 22.04 vm with 192.168.120.20 with Vagrant, to demo how static ip setup works for Ubuntu vm using Vagrantfile.
oldhorse@wsl2:$ cat Vagrantfile
$nic = <<SCRIPT
echo === $(date) Provisioning - nic $1 by $(whoami) start
SUBNET=$(echo $1 | cut -d"." -f1-3)
cat <<EOF | sudo tee /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: no
dhcp6: no
addresses: [$1/24]
routes:
- to: default
via: ${SUBNET}.1
nameservers:
addresses: [8.8.8.8,1.1.1.1]
EOF
sudo unlink /etc/resolv.conf
sudo rm /etc/resolv.conf
cat << EOF | sudo tee /etc/resolv.conf
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF
sudo chattr +i /etc/resolv.conf
cat /etc/netplan/01-netcfg.yaml
cat /etc/resolv.conf
sudo netplan apply
sleep 30
echo eth0 setting
ip addr
ip route
ping -c 2 google.ca
echo === $(date) Provisioning - nic $1 by $(whoami) end
SCRIPT
Vagrant.configure("2") do |config|
config.vm.box = "generic/ubuntu2204"
config.vm.provision "shell", inline: "sudo timedatectl set-timezone America/Montreal", privileged: false, run: "always"
config.ssh.insert_key = false
config.vm.box_check_update = false
config.vm.network "public_network", bridge: "myNAT"
config.vm.define "master" do |master|
master.vm.hostname = "ub22hpv"
master.vm.provider "hyperv" do |v|
v.vmname = "ub22hpv"
v.memory = 1024
end
master.vm.provision "shell", inline: $nic, args: "192.168.120.20", privileged: false
end
end
We can see vagrant trying to ssh to vm via ipv6 with private key,
master: SSH address: fe80::215:5dff:fec9:606c:22
master: SSH username: vagrant
master: SSH auth method: private key
Full vagrant up log as below,
oldhorse@wsl2: vagrant.exe up
Bringing machine 'master' up with 'hyperv' provider...
==> master: Verifying Hyper-V is enabled...
==> master: Verifying Hyper-V is accessible...
==> master: Importing a Hyper-V instance
master: Creating and registering the VM...
master: Successfully imported VM
master: Configuring the VM...
master: Setting VM Enhanced session transport type to disabled/default (VMBus)
==> master: Starting the machine...
==> master: Waiting for the machine to report its IP address...
master: Timeout: 120 seconds
master: IP: fe80::215:5dff:fec9:606c
==> master: Waiting for machine to boot. This may take a few minutes...
master: SSH address: fe80::215:5dff:fec9:606c:22
master: SSH username: vagrant
master: SSH auth method: private key
==> master: Machine booted and ready!
==> master: Setting hostname...
==> master: Running provisioner: shell...
master: Running: inline script
==> master: Running provisioner: shell...
master: Running: inline script
master: === Sun Apr 21 05:36:13 PM EDT 2024 Provisioning - nic 192.168.120.20 by vagrant start
master: network:
master: version: 2
master: renderer: networkd
master: ethernets:
master: eth0:
master: dhcp4: no
master: dhcp6: no
master: addresses: [192.168.120.20/24]
master: routes:
master: - to: default
master: via: 192.168.120.1
master: nameservers:
master: addresses: [8.8.8.8,1.1.1.1]
master: rm: cannot remove '/etc/resolv.conf': No such file or directory
master: nameserver 8.8.8.8
master: nameserver 1.1.1.1
master: network:
master: version: 2
master: renderer: networkd
master: ethernets:
master: eth0:
master: dhcp4: no
master: dhcp6: no
master: addresses: [192.168.120.20/24]
master: routes:
master: - to: default
master: via: 192.168.120.1
master: nameservers:
master: addresses: [8.8.8.8,1.1.1.1]
master: nameserver 8.8.8.8
master: nameserver 1.1.1.1
master:
master: ** (generate:3029): WARNING **: 17:36:14.124: Permissions for /etc/netplan/00-installer-config.yaml are too open. Netplan configuration should NOT be accessible by others.
master: eth0 setting
master: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
master: link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
master: inet 127.0.0.1/8 scope host lo
master: valid_lft forever preferred_lft forever
master: 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
master: link/ether 00:15:5d:c9:60:6c brd ff:ff:ff:ff:ff:ff
master: inet 192.168.120.20/24 brd 192.168.120.255 scope global eth0
master: valid_lft forever preferred_lft forever
master: inet6 fe80::215:5dff:fec9:606c/64 scope link
master: valid_lft forever preferred_lft forever
master: default via 192.168.120.1 dev eth0 proto static
master: 192.168.120.0/24 dev eth0 proto kernel scope link src 192.168.120.20
master: PING google.ca (142.250.65.163) 56(84) bytes of data.
master: 64 bytes from lga25s71-in-f3.1e100.net (142.250.65.163): icmp_seq=1 ttl=105 time=30.8 ms
master: 64 bytes from lga25s71-in-f3.1e100.net (142.250.65.163): icmp_seq=2 ttl=105 time=69.3 ms
master:
master: --- google.ca ping statistics ---
master: 2 packets transmitted, 2 received, 0% packet loss, time 1004ms
master: rtt min/avg/max/mdev = 30.791/50.024/69.258/19.233 ms
master: === Sun Apr 21 05:36:45 PM EDT 2024 Provisioning - nic 192.168.120.20 by vagrant end
Build up k8s cluster with Hyper-V using Vagrant
After we make hyper-v vm with static ip works, then it is possible to install Docker, k8s package and build up k8s cluster via provision inline script.
We assume it is 2 nodes k8s cluster:
- Ubuntu 22.04
- master: 2GB RAM 1 cpu 192.168.120.20
- worker: 1GB RAM 1 cpu 192.168.120.30
- Docker: latest version
- k8s: latest version , it is 1.30 here
It spent around 26 mins to build up k8s 2 nodes cluster very smoothly,
oldhorse@wsl2: date; vagrant.exe up ; date
Sun Apr 21 18:05:06 EDT 2024
Bringing machine 'master' up with 'hyperv' provider...
Bringing machine 'worker' up with 'hyperv' provider...
...
master: kubeadm join 192.168.120.20:6443 --token y10r7h.1vycfilas9kpovzp \
master: --discovery-token-ca-cert-hash sha256:64e627afc4ed15beb607adf4073ae856400097fb4fed248094fb679e235cc597
...
worker: This node has joined the cluster:
worker: * Certificate signing request was sent to apiserver and a response was received.
...
Sun Apr 21 18:31:33 EDT 2024
check k8s cluster, it is running as 2 nodes cluster.
We can ssh to master vm using vagrant, or directly ssh to 192.168.120.20 master and 192.168.120.30 worker.
vagrant ssh master
It looks good,
vagrant@master:~$ k get node
NAME STATUS ROLES AGE VERSION
master Ready control-plane 31m v1.30.0
worker Ready <none> 19m v1.30.0
vagrant@master:~$ k get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
calico-apiserver calico-apiserver-7868f6b748-glqdx 1/1 Running 0 11m
calico-apiserver calico-apiserver-7868f6b748-kc4gq 1/1 Running 0 11m
calico-system calico-kube-controllers-78788579b8-vvrdh 1/1 Running 0 30m
calico-system calico-node-dlrs8 1/1 Running 0 19m
calico-system calico-node-vvxm6 1/1 Running 0 30m
calico-system calico-typha-65bf4f686-mlpbx 1/1 Running 0 30m
kube-system coredns-76f75df574-65mk8 1/1 Running 0 31m
kube-system coredns-76f75df574-z982z 1/1 Running 0 31m
kube-system etcd-master 1/1 Running 0 31m
kube-system kube-apiserver-master 1/1 Running 0 31m
kube-system kube-controller-manager-master 1/1 Running 0 31m
kube-system kube-proxy-dcfcc 1/1 Running 0 19m
kube-system kube-proxy-x7qtl 1/1 Running 0 31m
kube-system kube-scheduler-master 1/1 Running 0 31m
tigera-operator tigera-operator-6fbc4f6f8d-h5wcm 1/1 Running 0
vagrant@master:~$ k run test --image=nginx $do
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: test
name: test
spec:
containers:
- image: nginx
name: test
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
vagrant@master:~$ k run test --image=nginx
pod/test created
vagrant@master:~$ k get pod
NAME READY STATUS RESTARTS AGE
test 0/1 ContainerCreating 0 4s
vagrant@master:~$ k get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test 1/1 Running 0 42s 192.168.171.67 worker <none> <none>
vagrant@master:~$ curl 192.168.171.67
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
</html>
k8s setup tool set
In above example, we demo the quick way to launch k8s cluster using Vagrant, which uses inline scripts to install docker, setup cri-dockerd, install k8s packages, init k8s cluster or join to cluster on worker node.
In fact we can use scripts for handy setup k8s cluster when you already have running Linux node, no matter it is physical Linux, Linux vm, or WSL2.
Just run script as sudo user,
master node
docker-server.sh
k8s-install.sh
cri-dockerd.sh
k8s-init.sh "192.168.120.20"
worker node
docker-server.sh
k8s-install.sh
cri-dockerd.sh
k8s-join-worker.sh "192.168.120.20"
Conclusion
We make the k8s 2 nodes cluster setup automation smoothly using Vagrant:
- it is possible to make k8s cluster setup on Hyper-V full automatically
- it is ideal local k8s lab for production like testing and education
About Me
Hey! I am Robert Wang, live in Montreal.
More simple and more efficient.