Co když vám řeknu, že můžeme zkombinovat to nejlepší (nebo nejšílenější?) z obou světů a spustit plnohodnotné Windows 11 přímo v Kubernetes clusteru běžícím na Linuxu?
Ano, čtete správně. Ne jako službu, ne jako webovou aplikaci, ale jako plnohodnotný desktopový systém, na který se můžete připojit přes vzdálenou plochu. Dnes se podíváme na elegantní skript, který přesně tohle umožňuje, a rozebereme si, jaká magie se za ním skrývá.
Je samozřejmě potřeba abyste si pak výsledný stroj legalizovali licenčně. 🙂 Ale to už není starost tohoto článku.

Proč by to proboha někdo dělal?
Než mě obviníte z kacířství, zamysleme se nad praktickými důvody:
- Testovací prostředí na jedno kliknutí: Potřebujete otestovat aplikaci na čisté instalaci Windows? Žádné zdlouhavé instalace. Spustíte skript, otestujete, smažete pod. Čisté, rychlé, opakovatelné.
- Vzdálená plocha jako služba: Chcete mít přístup ke svému Windows prostředí odkudkoli, bez nutnosti nechávat běžet fyzický počítač? Kubernetes se postará, aby váš „počítač“ byl vždy online.
- Automatizace GUI aplikací: Některé starší nebo specifické aplikace nemají API a musí se ovládat přes grafické rozhraní. Spuštěním v K8s je můžete snadno integrovat do moderních CI/CD pipelines.
Takže, teď když jsme si ospravedlnili tuto úžasnou zvrácenost, pojďme se podívat, jak to funguje.
Anatomie skriptu
Náš kouzelný proutek je bashový skript, který zautomatizuje celý proces. Nepůjdeme řádek po řádku jako při nudné školní besídce, ale podíváme se na jeho klíčové fáze.
Fáze 1: Konfigurace a příprava
Skript začíná definicí proměnných. Můžete si nastavit vše od názvu aplikace, přes velikost RAM (8G), počet jader CPU (4), až po velikost disku (64G). To nejdůležitější se ale děje zde:
Bash
DATA_DIR=“${DATA_DIR:-„$(pwd)/windows-data“}“
Tady skript říká, že veškerá data virtuálního stroje (tedy celý jeho disk) budou uložena v lokálním adresáři windows-data. Je to rychlé a jednoduché řešení, ale má to i své temné stránky, ke kterým se ještě dostaneme.
Fáze 2: Stavba „Matrjošky“
Tady přichází ten největší trik. My nespouštíme Windows přímo v kontejneru. To by bylo… komplikované. Místo toho:
- Postavíme Docker image: Skript použije Dockerfile (není zde vidět, ale je součástí řešení) k vytvoření Linuxového kontejneru.
- Uvnitř Linuxu běží QEMU/KVM: Tento kontejner v sobě nese kompletní virtualizační software.
- QEMU/KVM spouští Windows 11: Uvnitř kontejneru se tedy vytvoří plnohodnotný virtuální stroj, do kterého se automaticky stáhne a nainstaluje Windows 11.
Je to taková technologická matrjoška: Kubernetes Pod -> Linux Kontejner -> QEMU/KVM -> Windows 11. Geniální, že? Obraz tohoto kontejneru se následně nahraje do lokálního Docker registru (localhost:5000), aby ho Kubernetes mohl najít.
Fáze 3: Nasazení do Kubernetes
Teď přichází na řadu kubectl. Skript dynamicky generuje dva Kubernetes manifesty.
- Deployment:
Tento objekt říká Kubernetes: „Hele, chci, aby neustále běžel jeden pod s mým Windows kontejnerem.“ Zde jsou nejzajímavější části:
- privileged: true: Je to nutné, aby mohl přistupovat k hardwarové virtualizaci (/dev/kvm).
- volumeMounts: Zde se mapují zařízení z hostitelského stroje (/dev/kvm) a náš datový adresář (hostPath) do kontejneru. Použití hostPath znamená, že data jsou vázána na konkrétní uzel clusteru. Pokud se pod restartuje na jiném uzlu, o svá data přijdete.
- Service (Typ NodePort):
Tento objekt funguje jako směrovka. Vezme porty zevnitř kontejneru (jako RDP port 3389) a „vystrčí“ je ven na konkrétní porty na všech uzlech vašeho clusteru. Díky tomu se můžete na své nové Windows připojit z vaší lokální sítě.
Jak to celé spustit?
- Předpoklady: Musíte mít nainstalovaný Docker, kubectl a funkční Kubernetes cluster (např. Docker Desktop, Minikube, k3d). A také lokální Docker registry. Pokud ho nemáte, stačí spustit:
Bash
docker run -d -p 5000:5000 –restart=always –name registry registry:2 - Uložte skript: Zkopírujte celý kód a uložte ho jako run-windows.sh. Nezapomeňte na Dockerfile a další soubory, které projekt Kube-Virt (ze kterého tento přístup vychází) vyžaduje.
- Spusťte ho: Otevřete terminál a spusťte:
Bash
bash run-windows.sh - Počkejte a připojte se: První spuštění bude trvat déle, protože se musí stáhnout a nainstalovat celý systém Windows. Po dokončení skript vypíše porty (NodePort), přes které se můžete připojit. Pomocí klienta pro vzdálenou plochu (RDP) se pak připojíte na adresu vašeho Kubernetes uzlu a příslušný port.
Závěr:
Tento přístup je dokonalou ukázkou flexibility moderních technologií. Je to rychlý, automatizovaný a neuvěřitelně mocný nástroj pro vývojáře a testery.
Zároveň je to ale připomínka, že ne všechno, co jde udělat, by se mělo dělat v produkčním prostředí. Použití privileged kontejnerů a hostPath volume je skvělá zkratka pro lokální vývoj, ale v reálném nasazení byste měli sáhnout po robustnějších řešeních jako je Kubevirt a dedikované úložiště (Persistent Volumes).
Takže, směle do experimentování! A až se vás příště někdo zeptá, jestli běží Windows na Kubernetes, můžete s úsměvem říct: „Jistě, podrž mi kafe.“
Ať se bity práší!
Můj script je zde:
#!/usr/bin/env bash
set -euo pipefail
# — Konfigurace —
NAMESPACE=“${NAMESPACE:-default}“
APP_NAME=“${APP_NAME:-windows}“
DATA_DIR=“${DATA_DIR:-„$(pwd)/windows-data“}“
IMAGE_NAME=“${IMAGE_NAME:-${APP_NAME}-kvm}“
IMAGE_TAG=“${IMAGE_TAG:-11}“
BASE_VERSION_ARG=“${BASE_VERSION_ARG:-latest}“
REGISTRY_HOSTPORT=“${REGISTRY_HOSTPORT:-localhost:5000}“ # sem to vždy pushnem
LOCAL_IMAGE=“${IMAGE_NAME}:${IMAGE_TAG}“
FULL_IMAGE=“${REGISTRY_HOSTPORT}/${IMAGE_NAME}:${IMAGE_TAG}“
HTTP_NODEPORT=“${HTTP_NODEPORT:-30006}“
RDP_TCP_NODEPORT=“${RDP_TCP_NODEPORT:-30389}“
RDP_UDP_NODEPORT=“${RDP_UDP_NODEPORT:-30389}“
VNC_NODEPORT=“${VNC_NODEPORT:-30900}“
WIN_VERSION=“${WIN_VERSION:-11}“
RAM_SIZE=“${RAM_SIZE:-8G}“
CPU_CORES=“${CPU_CORES:-4}“
DISK_SIZE=“${DISK_SIZE:-64G}“
# — Kontroly —
command -v docker >/dev/null || { echo „Chybí docker.“ >&2; exit 1; }
command -v kubectl >/dev/null || { echo „Chybí kubectl.“ >&2; exit 1; }
ARCH=“$(uname -m)“
case „$ARCH“ in
x86_64) K8S_ARCH=“amd64″; PLAT=“linux/amd64″ ;;
aarch64|arm64) K8S_ARCH=“arm64″; PLAT=“linux/arm64″ ;;
*) echo „Nepodporovaná architektura: $ARCH“ >&2; exit 1 ;;
esac
echo „Vytvářím adresář pro perzistentní data v: ${DATA_DIR}“
mkdir -p „${DATA_DIR}“
# — Dočasný build kontext v /tmp (repo se nechá být) —
BUILD_CTX=“$(mktemp -d -t winbuild.XXXXXX)“
trap ‚rm -rf „$BUILD_CTX“‚ EXIT
cp -a Dockerfile „${BUILD_CTX}/“
[[ -d src ]] && cp -a src „${BUILD_CTX}/“
[[ -d assets ]] && cp -a assets „${BUILD_CTX}/“
# .dockerignore jen v dočasném kontextu (ignoruje windows-data atd.)
cat > „${BUILD_CTX}/.dockerignore“ <<‚EODI‘
windows-data/
.git/
*.iso
*.img
*.qcow2
*.vhd*
*.vmdk
*.log
*.tmp
*.swp
__pycache__/
*.py[cod]
.venv/
env/
EODI
# — Build & push na localhost:5000 —
echo „Stavím image ${LOCAL_IMAGE} (platform: ${PLAT}, VERSION_ARG=${BASE_VERSION_ARG})“
if docker buildx version >/dev/null 2>&1; then
docker buildx build –platform „${PLAT}“ -t „${LOCAL_IMAGE}“ –build-arg VERSION_ARG=“${BASE_VERSION_ARG}“ –load „${BUILD_CTX}“
else
docker build -t „${LOCAL_IMAGE}“ –build-arg VERSION_ARG=“${BASE_VERSION_ARG}“ „${BUILD_CTX}“
fi
echo „Taguji a pushuji ${FULL_IMAGE}“
docker tag „${LOCAL_IMAGE}“ „${FULL_IMAGE}“
docker push „${FULL_IMAGE}“ || {
echo „Push na ${REGISTRY_HOSTPORT} selhal. Spusť (pokud neběží):“
echo “ docker run -d -p 5000:5000 –restart=always –name registry registry:2″
exit 1
}
# — K8s: jen Deployment + Service, ŽÁDNÉ PV/PVC —
echo „Aplikuji Deployment…“
kubectl -n „${NAMESPACE}“ apply -f – <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}
labels:
app: ${APP_NAME}
spec:
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
template:
metadata:
labels:
app: ${APP_NAME}
spec:
nodeSelector:
kubernetes.io/arch: ${K8S_ARCH}
terminationGracePeriodSeconds: 120
containers:
– name: ${APP_NAME}
image: ${FULL_IMAGE}
imagePullPolicy: IfNotPresent
env:
– name: VERSION
value: „${WIN_VERSION}“
– name: RAM_SIZE
value: „${RAM_SIZE}“
– name: CPU_CORES
value: „${CPU_CORES}“
– name: DISK_SIZE
value: „${DISK_SIZE}“
ports:
– containerPort: 8006
name: http
protocol: TCP
– containerPort: 3389
name: rdp-tcp
protocol: TCP
– containerPort: 3389
name: rdp-udp
protocol: UDP
– containerPort: 5900
name: vnc
protocol: TCP
securityContext:
privileged: true
capabilities:
add: [„NET_ADMIN“]
volumeMounts:
– mountPath: /storage
name: storage
– mountPath: /dev/kvm
name: dev-kvm
– mountPath: /dev/net/tun
name: dev-tun
volumes:
– name: storage
hostPath:
path: „${DATA_DIR}“
type: Directory
– name: dev-kvm
hostPath:
path: /dev/kvm
type: CharDevice
– name: dev-tun
hostPath:
path: /dev/net/tun
type: CharDevice
EOF
echo „Aplikuji Service…“
kubectl -n „${NAMESPACE}“ apply -f – <<EOF
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}
spec:
type: NodePort
selector:
app: ${APP_NAME}
ports:
– name: http
protocol: TCP
port: 8006
targetPort: 8006
nodePort: ${HTTP_NODEPORT}
– name: rdp-tcp
protocol: TCP
port: 3389
targetPort: 3389
nodePort: ${RDP_TCP_NODEPORT}
– name: rdp-udp
protocol: UDP
port: 3389
targetPort: 3389
nodePort: ${RDP_UDP_NODEPORT}
– name: vnc
protocol: TCP
port: 5900
targetPort: 5900
nodePort: ${VNC_NODEPORT}
EOF
echo „Hotovo.“
echo „Pods: kubectl -n ${NAMESPACE} get pods -l app=${APP_NAME}“
echo „Služby: ${HTTP_NODEPORT} (HTTP), ${RDP_TCP_NODEPORT}/${RDP_UDP_NODEPORT} (RDP), ${VNC_NODEPORT} (VNC)“
echo „Mount: hostPath -> ${DATA_DIR} -> /storage (žádné PV/PVC)“
![]()