~/2026/07/08/kubernetes-running-talos-alongside-a-legacy-hyper-v-estate.md
Kubernetes: Running Talos Alongside a Legacy Hyper-V Estate
--- author: Tom Lasswell date: updated: read: 7 min in: [engineering] tags: [talos, kubernetes, hyper-v] ---
$ grep -n '^#' post.md
Most of the Kubernetes-on-Hyper-V advice I've read assumes you're starting from a clean slate or migrating everything to a new hypervisor. Neither describes reality when you're supporting a Windows estate that has years of Hyper-V clusters, System Center tooling, and operational habits built around VMM and Failover Clustering. Ripping that out to make room for Kubernetes was never on the table. What worked was treating Talos Linux as just another guest OS on the existing Hyper-V hosts, and being deliberate about where the two worlds have to touch.
Everything here is written against Talos 1.14. Hyper-V is on Talos' list of supported virtualized platforms, and there's a Hyper-V page in the Talos docs, but it's a lab walkthrough built on a community PowerShell module. What follows is how I turned that into something that fits a managed estate.
Talos as a guest, not a replacement
Talos ships as an ISO from the Image Factory, and as far as Talos is concerned Hyper-V is an ordinary hypervisor: the VM boots into maintenance mode, gets a machine config applied over the network, and comes up as an immutable, API-managed node with no SSH and no package manager to drift. The settings that took adjustment were on the Hyper-V side, and once they were captured in a script, a new node became a five-minute task instead of a bespoke one.
First, the image. I build a schematic that adds the official hyperv-guest-agent system extension, which provides the Hyper-V KVP daemon (it reports the guest's IP, hostname and OS to the host, so Get-VMNetworkAdapter shows the address) and the VSS daemon:
# schematic.yaml
customization:
systemExtensions:
officialExtensions:
- siderolabs/hyperv-guest-agent
curl -X POST --data-binary @schematic.yaml https://factory.talos.dev/schematics
# {"id":"<schematic-id>","schematic":"customization:\n systemExtensions: ..."}
SCHEMATIC_ID="<schematic-id>" # the id from the response
# ISO for the VMs, and the installer image the machine config must reference
curl -LO "https://factory.talos.dev/image/${SCHEMATIC_ID}/v1.14.0/metal-amd64.iso"
echo "factory.talos.dev/metal-installer/${SCHEMATIC_ID}:v1.14.0"
Schematics are content-addressed, so the same YAML always yields the same ID. Keep that ID with the cluster's patches: from Talos 1.14 on, installer images come from the Image Factory, and every future talosctl upgrade has to reference the same schematic or the node loses its extensions.
Then the VM. The shape follows what the module linked from the Talos Hyper-V guide does (Generation 2, Secure Boot off, ISO as the first boot device) plus the settings a shared estate needs:
- Secure Boot off. The standard Talos ISO isn't a SecureBoot image; Talos publishes separate
-securebootassets signed with Sidero Labs' own key, and enrolling that key requires the firmware to be in setup mode. The Talos docs don't cover doing that on Hyper-V, so I don't half-configure it: Secure Boot is explicitly off. - A static MAC per VM, from a range reserved for the cluster. Talos can then match the NIC by MAC with a
LinkAliasConfiginstead of trusting interface naming, and the network team's reservations never go stale. - An access-mode VLAN on the vNIC, so the node lands on the cluster VLAN with no tagging inside the guest.
- Static memory and no checkpoints. Rolling an etcd member's disk back to an older point in time is never what you want, and I'd rather Kubernetes see a fixed memory size than a balloon.
- Sizing from Talos' system requirements. Control plane nodes get at least the recommended 4 cores, 4 GiB and 100 GiB. Talos' amd64 images also require a CPU that exposes the x86-64-v2 microarchitecture level; a VM below that level halts at boot with a message saying so.
This script builds the VM and writes the node's Talos patch at the same time, so the MAC in the config is the MAC on the VM:
#Requires -Modules Hyper-V
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Creates a Talos Linux VM on a Hyper-V host and writes its Talos node patch.
.DESCRIPTION
Builds a Generation 2 VM with Secure Boot off, a static MAC address, an
access-mode VLAN, static memory, checkpoints disabled and the Talos ISO as
the first boot device. Writes patches/nodes/<Name>.yaml with a hostname,
a link alias pinned to the static MAC, and the node's static address, so
the Talos config matches the VM without anyone copying MACs by hand.
Run it on the Hyper-V host (or a cluster node that owns the storage path).
.PARAMETER Name
VM name and Talos hostname, for example talos-cp1.
.PARAMETER IPAddress
Static address in CIDR form, for example 10.20.30.11/24.
.PARAMETER Gateway
Default gateway for the node.
.PARAMETER StaticMacAddress
MAC address without separators, for example 00155D1E1E0B. Keep it inside
a range your DHCP and IPAM teams have reserved for the cluster.
.PARAMETER SwitchName
External virtual switch the cluster VLAN is trunked to.
.PARAMETER VlanId
Access VLAN for the Talos node NIC.
.PARAMETER IsoPath
Path to the Talos ISO (from the Image Factory schematic) on the host.
.PARAMETER VMPath
Folder for the VM configuration and its VHDX files.
.PARAMETER ProcessorCount
Virtual processors. Talos recommends 4 for control plane nodes.
.PARAMETER MemoryBytes
Static memory. Talos recommends 4 GiB for control plane nodes; the 8 GB
default leaves room for workloads on nodes that also run pods.
.PARAMETER OsDiskBytes
System disk size. Talos recommends 100 GiB.
.PARAMETER DataDiskBytes
Optional second VHDX for workload storage (for example a CSI backend).
.PARAMETER PatchDirectory
Where to write the node patch. Defaults to .\patches\nodes.
.EXAMPLE
.\New-TalosHyperVNode.ps1 -Name talos-cp1 -IPAddress 10.20.30.11/24 -Gateway 10.20.30.1 -StaticMacAddress 00155D1E1E0B -SwitchName vSwitch-Trunk -VlanId 230 -IsoPath C:\ISO\talos-hyperv-amd64.iso -VMPath C:\ClusterStorage\Volume1\Talos
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)] [string] $Name,
[Parameter(Mandatory)] [ValidatePattern('^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$')] [string] $IPAddress,
[Parameter(Mandatory)] [string] $Gateway,
[Parameter(Mandatory)] [ValidatePattern('^[0-9A-Fa-f]{12}$')] [string] $StaticMacAddress,
[Parameter(Mandatory)] [string] $SwitchName,
[Parameter(Mandatory)] [ValidateRange(1, 4094)] [int] $VlanId,
[Parameter(Mandatory)] [string] $IsoPath,
[Parameter(Mandatory)] [string] $VMPath,
[int] $ProcessorCount = 4,
[long] $MemoryBytes = 8GB,
[long] $OsDiskBytes = 100GB,
[long] $DataDiskBytes = 0,
[string] $PatchDirectory = (Join-Path -Path (Get-Location) -ChildPath 'patches\nodes')
)
$ErrorActionPreference = 'Stop'
if (Get-VM -Name $Name -ErrorAction SilentlyContinue) {
throw "A VM named $Name already exists on this host."
}
$vhdFolder = Join-Path -Path $VMPath -ChildPath "$Name\Virtual Hard Disks"
$osDiskPath = Join-Path -Path $vhdFolder -ChildPath "$Name-os.vhdx"
if ($PSCmdlet.ShouldProcess($Name, 'Create Talos VM')) {
New-Item -Path $vhdFolder -ItemType Directory -Force | Out-Null
New-VM -Name $Name -Generation 2 -MemoryStartupBytes $MemoryBytes -NewVHDPath $osDiskPath `
-NewVHDSizeBytes $OsDiskBytes -SwitchName $SwitchName -Path $VMPath | Out-Null
# Fixed CPU and memory, and no checkpoints: reverting an etcd member or a
# node's disk to an older point in time is never what you want.
Set-VM -Name $Name -ProcessorCount $ProcessorCount -StaticMemory -MemoryStartupBytes $MemoryBytes `
-CheckpointType Disabled -AutomaticCheckpointsEnabled $false
Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $StaticMacAddress
Set-VMNetworkAdapterVlan -VMName $Name -Access -VlanId $VlanId
if ($DataDiskBytes -gt 0) {
$dataDiskPath = Join-Path -Path $vhdFolder -ChildPath "$Name-data.vhdx"
New-VHD -Path $dataDiskPath -SizeBytes $DataDiskBytes -Dynamic | Out-Null
Add-VMHardDiskDrive -VMName $Name -Path $dataDiskPath
}
# The standard Talos ISO is not a SecureBoot image (Talos publishes separate
# -secureboot assets), so Secure Boot is off, as in the Talos Hyper-V guide.
Add-VMDvdDrive -VMName $Name -Path $IsoPath
Set-VMFirmware -VMName $Name -EnableSecureBoot Off -FirstBootDevice (Get-VMDvdDrive -VMName $Name)
}
# Talos matches the NIC by MAC, in lowercase colon-separated form.
$macForTalos = (($StaticMacAddress.ToLower() -split '(..)') -ne '') -join ':'
$patch = @"
apiVersion: v1alpha1
kind: HostnameConfig
hostname: $Name
auto: off
---
apiVersion: v1alpha1
kind: LinkAliasConfig
name: net0
selector:
match: mac(link.permanent_addr) == "$macForTalos"
---
apiVersion: v1alpha1
kind: LinkConfig
name: net0
addresses:
- address: $IPAddress
routes:
- gateway: $Gateway
"@
New-Item -Path $PatchDirectory -ItemType Directory -Force | Out-Null
$patchPath = Join-Path -Path $PatchDirectory -ChildPath "$Name.yaml"
# WriteAllText writes UTF-8 without a byte order mark on every PowerShell version.
[System.IO.File]::WriteAllText($patchPath, $patch)
if ($PSCmdlet.ShouldProcess($Name, 'Start VM')) {
Start-VM -Name $Name
}
[pscustomobject]@{
Name = $Name
MacAddress = $macForTalos
VlanId = $VlanId
Address = $IPAddress
Patch = $patchPath
}
Running it for the first control plane node looks like this:
Name : talos-cp1
MacAddress : 00:15:5d:1e:1e:0b
VlanId : 230
Address : 10.20.30.11/24
Patch : C:\talos\lab\patches\nodes\talos-cp1.yaml
After the config is applied and the node has installed itself to disk, remove the DVD drive. The Talos Hyper-V guide warns that Talos might fail to boot if the ISO stays attached:
Get-VMDvdDrive -VMName talos-cp1 | Remove-VMDvdDrive
Set-VMFirmware -VMName talos-cp1 -FirstBootDevice (Get-VMHardDiskDrive -VMName talos-cp1 -ControllerLocation 0)
If you already run Hyper-V failover clusters, the Talos VMs are just more cluster roles. The same placement rules that keep two domain controllers off one host apply to control plane nodes: three etcd members on one host is one host failure away from losing quorum. Hyper-V: Deploying a Two-Node Failover Cluster on a Budget covers the host side.
Networking is where the two estates actually meet
The existing estate has its own VLAN structure, its own DHCP scopes, and firewall rules built around the assumption that everything on a given VLAN is a traditional Windows or Linux VM with predictable, mostly-static behavior. Kubernetes doesn't share that assumption: pods come and go, services get new endpoints, and a CNI like Cilium makes its own routing decisions on top of whatever the physical network provides.
So the Talos nodes live on a dedicated VLAN (10.20.30.0/24 in these examples), and the cluster's pod and service ranges are picked from space that isn't routed anywhere else in the estate. The shared cluster patch sets those ranges, adds a Layer 2 VIP for the Kubernetes API (the control plane nodes elect an owner through etcd, so no load balancer is needed), removes the default Flannel CNI so Cilium can be installed per Talos' Cilium guide, and pins the installer image to the schematic:
# patches/controlplane.yaml
apiVersion: v1alpha1
kind: KubeNetworkConfig
podSubnets:
- 172.28.0.0/16
serviceSubnets:
- 172.29.0.0/16
---
apiVersion: v1alpha1
kind: KubeFlannelCNIConfig
$patch: delete
---
apiVersion: v1alpha1
kind: Layer2VIPConfig
name: 10.20.30.10
link: net0
---
apiVersion: v1alpha1
kind: UnattendedInstallConfig
installer:
image: factory.talos.dev/metal-installer/<schematic-id>:v1.14.0
provisioning:
diskSelector:
match: disk.dev_path == "/dev/sda"
The subnet lists replace the defaults on merge rather than appending, per the patching guide, and the Flannel removal is the documented Talos 1.14 way to set the CNI to none. The VIP must come from the nodes' own subnet and must not be handed out by DHCP. The VIP documentation also says not to use it as a talosconfig endpoint, because it disappears exactly when etcd or the API server is broken, so talosctl points at the three node addresses.
For the network team, the useful artifact isn't a Kubernetes diagram. It's the port list. Talos' ingress firewall guide gives it: apid on TCP 50000, trustd on TCP 50001, the Kubernetes API on TCP 6443, etcd on TCP 2379-2380 between control plane nodes only, kubelet on TCP 10250 inside the cluster, and the CNI's VXLAN port (UDP 8472 for Cilium, 4789 for Flannel). I enforce the same thing on the nodes with Talos' own ingress firewall, so the cluster doesn't depend solely on the estate's ACLs. The doc's example opens apid and the API to everyone; I narrow both to the cluster VLAN plus the management subnet the jump hosts live in:
# patches/firewall-controlplane.yaml
apiVersion: v1alpha1
kind: NetworkDefaultActionConfig
ingress: block
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: apid-ingress
portSelector:
ports:
- 50000
protocol: tcp
ingress:
- subnet: 10.20.30.0/24
- subnet: 10.20.0.0/24
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: kubernetes-api-ingress
portSelector:
ports:
- 6443
protocol: tcp
ingress:
- subnet: 10.20.30.0/24
- subnet: 10.20.0.0/24
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: trustd-ingress
portSelector:
ports:
- 50001
protocol: tcp
ingress:
- subnet: 10.20.30.0/24
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: kubelet-ingress
portSelector:
ports:
- 10250
protocol: tcp
ingress:
- subnet: 10.20.30.0/24
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: etcd-ingress
portSelector:
ports:
- 2379-2380
protocol: tcp
ingress:
- subnet: 10.20.30.11/32
- subnet: 10.20.30.12/32
- subnet: 10.20.30.13/32
---
apiVersion: v1alpha1
kind: NetworkRuleConfig
name: cni-vxlan
portSelector:
ports:
- 8472
protocol: udp
ingress:
- subnet: 10.20.30.0/24
Roll a default-block firewall out with --mode=try first. If a rule is wrong and you lock yourself out, Talos reverts the change after the timeout instead of leaving you at the Hyper-V console of a node with no shell.
Ingress for applications is the one deliberate, documented crossing point between "legacy VLAN" and "cluster network." Everything else in the estate talks to the cluster through that door rather than learning about pod IPs.
Storage means picking your dependency deliberately
Hyper-V's storage story (CSVs, Storage Spaces Direct, whatever SAN sits behind it) and Kubernetes' storage story (CSI drivers, StorageClasses, PersistentVolumes) don't talk to each other by default. I didn't want Talos nodes depending on the same storage fabric in a way that could let a Kubernetes storage problem cascade into the Hyper-V estate or the other way round. Talos node OS disks are ordinary VHDX files on existing storage; that part is fine, it's just another VM disk. Persistent workload storage inside the cluster goes through a CSI driver with its own backend, kept operationally separate from the Hyper-V storage stack. Talos' storage guide walks through the options it documents on Talos (Rook/Ceph, Longhorn, Mayastor, Piraeus/LINSTOR, SeaweedFS, and vendor drivers such as Dell PowerStore and Synology). The -DataDiskBytes parameter in the script exists for the replicated options that want a raw disk per node. That separation costs some duplication, but a bad day in one storage system doesn't automatically become a bad day in the other.
The estate doesn't need to know Kubernetes exists
The System Center and VMM tooling that manages the rest of the fleet doesn't understand Talos nodes as anything other than VMs, and I've left it that way on purpose. Talos' API and talosctl handle node lifecycle, upgrades and configuration; VMM's job is limited to "this VM exists, is powered on, and lives on the right host," the same as any other guest. Two estate processes needed an explicit exception: patch orchestration and backup. Host patching treats the Talos VMs like any other clustered role, which is fine as long as only one host (and so at most one control plane node) is out at a time, but Talos OS upgrades go through talosctl upgrade, never through the Windows patch cycle. And VM-level backup of Talos nodes is the wrong unit: what you need back after a disaster is the secrets.yaml, the patches, and an etcd snapshot (talosctl etcd snapshot), not a VHDX of a node that can be rebuilt from its config in minutes. Trying to make the legacy tooling Kubernetes-aware would have meant fighting both toolchains at once. Keeping the boundary sharp let each one do the job it's actually good at.
References
- Hyper-V (Talos platform guide)
- Image Factory and Upgrading Talos Linux (Factory installer images from 1.14)
- SecureBoot
- System Requirements
- Virtual (shared) IP and Link Aliases
- Ingress Firewall
- Deploy Cilium CNI and Storage
- Siderolabs extensions catalog (
hyperv-guest-agent) - Microsoft Hyper-V module: New-VM, Set-VM, Set-VMFirmware, Set-VMNetworkAdapter