Kubernetes для самых маленьких. Часть 38: Собираем гибридный кластер на kubeadm
Все части серии «Kubernetes для самых маленьких»В части 36 мы разобрали, чем Windows-узел отличается от Linux-узла, а в прошлой части — как устроена его сеть и почему для Windows-воркеров удобнее отдельный кластер, сразу собранный под требования Calico для Windows, чем перевод работающего. Сегодня этот кластер соберём и посмотрим вживую на всё, о чём шла речь в двух прошлых частях. Весь блок о гибридном кластере работает на нём, а основной учебный кластер серии остаётся как был — с VXLANCrossSubnet и BGP.
Часть состоит из трёх разделов. Первый — Linux-основа тестового кластера: мастер и два Linux-воркера ставятся точно так же, как в части 4, поэтому здесь только отличия, а Calico с первого дня ставится в режиме, который понимает Windows. Второй — Windows-воркер k8s-lab-win-1.example.com на Windows Server 2025 в установке Server Core. Путь тот же, что для Linux-узлов: подготовить ОС, поставить containerd, kubelet и kubeadm, выполнить kubeadm join, дать узлу сеть. Отличия — в деталях: службы Windows вместо systemd, HostProcess-поды вместо привилегированных контейнеров, HNS вместо netfilter, а Linux-подам нужно явно запретить ехать на новый узел. Третий — проверка и разбор: Windows-под, связность между ОС и то, как всё это выглядит изнутри узла.
Стенд
| Узел | Адрес | Роль | Ресурсы |
|---|---|---|---|
k8s-lab-master-1.example.com |
192.168.10.23 | control plane, Ubuntu Server 26.04 | 2 vCPU, 4 ГБ, 32 ГБ |
k8s-lab-node-1.example.com |
192.168.10.24 | Linux-воркер, Ubuntu Server 26.04 | 2 vCPU, 8 ГБ, 32 ГБ |
k8s-lab-node-2.example.com |
192.168.10.26 | Linux-воркер, Ubuntu Server 26.04 | 2 vCPU, 8 ГБ, 32 ГБ |
k8s-lab-win-1.example.com |
192.168.10.25 | Windows-воркер, Windows Server 2025 Core | 4 vCPU, 8 ГБ, 64 ГБ |
| Сеть | Тестовый кластер | Основной кластер (для сравнения) |
|---|---|---|
| сеть узлов | 192.168.10.0/24, шлюз 192.168.10.1 (router.example.com) |
та же |
| сеть подов | 172.21.0.0/16, блоки /26 |
172.20.0.0/16 |
| сеть сервисов | 10.96.0.0/12, API 10.96.0.1, DNS 10.96.0.10 |
та же |
| адрес API | k8s-lab-api.example.com:6443 → 192.168.10.23 |
k8s-api.example.com:6443 |
| доставка между узлами | VXLAN всегда, BGP выключен | VXLANCrossSubnet и BIRD |
| Компонент | Linux-узлы | Windows-узел |
|---|---|---|
| ОС | Ubuntu Server 26.04 | Windows Server 2025 Datacenter, Server Core, сборка 10.0.26100 |
| Kubernetes | 1.37.2 | 1.37.2 |
| containerd | 2.2.2 из пакетов Ubuntu | 2.2.2, служба Windows |
| Calico | 3.33.0, Tigera operator, датаплейн nftables | 3.33.0 (Felix и CNI для Windows), HostProcess-под |
| kube-proxy | режим nftables |
режим kernelspace (winkernel), HostProcess-под из DaemonSet |
Сеть подов у тестового кластера своя, чтобы его адреса в выводе не путались с адресами основного; сеть сервисов совпадает — кластеры между собой не маршрутизируют, и пересечение диапазонов сервисов им не мешает. Kubernetes сразу 1.37.2, на которую основной кластер перешёл в части 35: нет смысла ставить 1.36, чтобы тут же обновлять.
Почему в тестовом кластере два Linux-воркера. Windows-узел умеет запускать только Windows-контейнеры, а почти всё служебное в кластере существует только в виде Linux-образов: CoreDNS, Tigera operator, Typha и calico-kube-controllers, вебхуки (cert-manager, Calico, позже — вебхук gMSA), metrics-server, контроллеры CSI. Документация Calico для Windows прямо требует хотя бы один Linux-воркер, на котором работают общекластерные компоненты Calico. Держать всё это на мастере, сняв с него taint, можно, но тогда тестовый кластер разошёлся бы с основным в главном — в разделении ролей узлов. Одного воркера хватило бы, чтобы кластер заработал, но тогда его не обслужить: kubectl drain единственного Linux-воркера (часть 35) оставил бы CoreDNS, вебхуки и контроллеры без места. Поэтому рядом с мастером два Linux-воркера, k8s-lab-node-1 и k8s-lab-node-2, как в основном кластере; для отказоустойчивости Typha Calico советует три Linux-воркера, но тестовому кластеру хватит двух.
рабочая станция: kubectl, контекст lab
│ 6443
▼
┌─ k8s-lab-master-1 192.168.10.23 ──────────────────────────┐
│ etcd, kube-apiserver, scheduler, controller-manager │
│ имя API: k8s-lab-api.example.com │
└────────────────────────────┬──────────────────────────────┘
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌─ k8s-lab-node-1 .24 ─┐ ┌─ k8s-lab-node-2 .26 ─┐ ┌─ k8s-lab-win-1 .25 ──┐
│ Ubuntu Server 26.04 │ │ Ubuntu Server 26.04 │ │ Windows Server 2025 │
│ служебные поды │ │ служебные поды │ │ Core, aspnetapp │
│ и поды hello │ │ и поды hello │ │ taint os=windows │
│ 172.21.211.0/26 │ │ 172.21.172.0/26 │ │ 172.21.41.0/26 │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
между всеми узлами — VXLAN: UDP 4789, VNI 4096
Адреса узлов на схеме — последний октет в сети 192.168.10.0/24, под ними — блоки адресов подов, которые Calico выдаст узлам. Как поставить Server Core и работать с ним из PowerShell, разобрано в первой части серии про Windows Server. Процессоров у Windows-узла вдвое больше, чем у Linux-воркеров: документация Kubernetes называет разумной отправной точкой 4 ядра, 8 ГБ памяти и 50 ГБ диска — сама ОС занимает больше, а образы Windows весят сотни мегабайт и гигабайты. Памяти Linux-воркерам дано столько же с запасом: в части 39 на них встанут Prometheus и Loki.
В DNS нужны записи четырёх узлов и k8s-lab-api.example.com → 192.168.10.23 — то же отдельное имя API, что в части 4: если тестовый кластер когда-нибудь получит второй мастер, имя перенаправят на виртуальный адрес, не пересобирая кластер.
Linux-узлы: как в части 4
Подготовка, containerd и пакеты
На k8s-lab-master-1, k8s-lab-node-1 и k8s-lab-node-2 всё делается по части 4, разделы «Подготовка узлов», «containerd» и «kubeadm, kubelet и kubectl», с такими отличиями:
- имена и адреса —
hostnamectl set-hostname k8s-lab-master-1,k8s-lab-node-1иk8s-lab-node-2, адреса 192.168.10.23, .24 и .26 в netplan; проверки уникальностиproduct_uuidи/etc/machine-idособенно важны, если машины склонированы из того же шаблона, что и узлы основного кластера; - swap, модули
overlayиbr_netfilter, sysctl, chrony сrouter.example.com— без изменений; - containerd — тот же пакет
containerdиз репозитория Ubuntu,SystemdCgroup = true; образsandboxвыставляем тем, что назовётkubeadm config images list --kubernetes-version v1.37.2, — по той же причине, что в части 4: kubeadm предупреждает о расхождении образа pause; - доверие реестру — настраивать не нужно: образы
helloтестовый кластер берёт из того же частного пакета вghcr.io, что и основной, а сертификат у ghcr.io публичный (часть 5); - репозиторий пакетов — сразу
v1.37:
user@k8s-lab-node-1:~$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
user@k8s-lab-node-1:~$ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /' \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
user@k8s-lab-node-1:~$ sudo apt-get update && sudo apt-get install -y kubelet kubeadm kubectl
user@k8s-lab-node-1:~$ sudo apt-mark hold kubelet kubeadm kubectl
user@k8s-lab-node-1:~$ kubeadm version -o short
v1.37.2
Файрвол узлов, как и в основном кластере, выключен; из таблицы портов части 4 тестовому кластеру не нужен 179/TCP — BGP здесь не будет, — а 4789/UDP теперь нужен между всеми узлами, а не только между подсетями.
Конфигурация и kubeadm init
kubeadm-config.yaml на мастере отличается от файла части 4 адресом, именами, версией и сетью подов:
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 192.168.10.23
bindPort: 6443
nodeRegistration:
name: k8s-lab-master-1
criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.37.2
controlPlaneEndpoint: k8s-lab-api.example.com:6443
networking:
podSubnet: 172.21.0.0/16
serviceSubnet: 10.96.0.0/12
dnsDomain: cluster.local
apiServer:
certSANs:
- k8s-lab-api.example.com
- k8s-lab-master-1.example.com
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
serverTLSBootstrap: true
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: nftables
Формат конфигурации kubeadm в 1.37 по-прежнему v1beta4, смысл всех четырёх документов разобран в части 4, раздел «Конфигурация кластера». Два поля здесь важны для Windows-узла. serverTLSBootstrap: true попадёт в общий KubeletConfiguration и на Windows-узел тоже: его kubelet запросит серверный сертификат у CA кластера, и metrics-server сможет снимать с него метрики, проверяя сертификат. controlPlaneEndpoint — имя, к которому будут обращаться не только узлы, но и компоненты Calico на Windows, об этом ниже.
user@k8s-lab-master-1:~$ sudo kubeadm config validate --config kubeadm-config.yaml
ok
user@k8s-lab-master-1:~$ sudo kubeadm init --config kubeadm-config.yaml
[init] Using Kubernetes version: v1.37.2
[preflight] Running pre-flight checks
...
[certs] apiserver serving cert is signed for DNS names [k8s-lab-api.example.com k8s-lab-master-1 k8s-lab-master-1.example.com kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.10.23]
...
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Your Kubernetes control-plane has initialized successfully!
...
kubeadm join k8s-lab-api.example.com:6443 --token 3h8q2w.k5n9v1c7x4m6z0p8 \
--discovery-token-ca-cert-hash sha256:7b2e9d4a1c6f8b3e0d5a2c7f9e4b1d6a8c3f0e5b2d7a9c4e1f6b8d3a0c5e2f7b
Фазы те же, что в части 4, раздел «kubeadm init и его фазы». У тестового кластера свой CA — новый корень доверия, — поэтому и хеш sha256:… другой: сертификаты основного и тестового кластеров друг другу не доверяют. super-admin.conf так же уносим с мастера в надёжное место.
Второй контекст в kubeconfig
С рабочей станции теперь нужно ходить в два кластера. kubeconfig для этого и устроен: в одном файле несколько кластеров, пользователей и контекстов, kubectl работает с текущим контекстом, а ключ --context выбирает другой на одну команду.
Первое, что приходит в голову, — положить admin.conf тестового кластера рядом и перечислить оба файла в переменной KUBECONFIG. Так делать не стоит. kubeadm в обоих кластерах называет записи одинаково: кластер kubernetes, пользователь kubernetes-admin, контекст kubernetes-admin@kubernetes. При слиянии файлов из одинаковых имён побеждает первое, и «второй» контекст молча получит адрес и учётные данные первого кластера. Поэтому переносим содержимое admin.conf под своими именами — командами kubectl config.
Копируем admin.conf с мастера, как в части 4, раздел «Доступ с рабочей станции»:
user@k8s-lab-master-1:~$ sudo install -m 600 -o user -g user /etc/kubernetes/admin.conf ~/admin.conf
$ LAB=~/.kube/lab-admin.conf
$ scp user@k8s-lab-master-1.example.com:admin.conf "$LAB" && chmod 600 "$LAB"
$ ssh user@k8s-lab-master-1.example.com rm admin.conf
$ field() { kubectl --kubeconfig "$LAB" config view --raw -o jsonpath="$1"; }
$ kubectl config set-cluster lab --server=https://k8s-lab-api.example.com:6443
Cluster "lab" set.
$ kubectl config set clusters.lab.certificate-authority-data "$(field '{.clusters[0].cluster.certificate-authority-data}')"
Property "clusters.lab.certificate-authority-data" set.
$ kubectl config set users.lab-admin.client-certificate-data "$(field '{.users[0].user.client-certificate-data}')"
Property "users.lab-admin.client-certificate-data" set.
$ kubectl config set users.lab-admin.client-key-data "$(field '{.users[0].user.client-key-data}')"
Property "users.lab-admin.client-key-data" set.
$ kubectl config set-context lab --cluster=lab --user=lab-admin
Context "lab" created.
$ shred -u "$LAB"
set-cluster lab— запись в спискеclustersс адресом API тестового кластера;certificate-authority-data— его CA, которымkubectlпроверяет сертификат kube-apiserver.users.lab-admin— клиентский сертификат и ключ администратора тестового кластера.kubectl config setдля полей*-dataожидает base64 и сам его декодирует перед записью, а вadmin.confзначения уже лежат в base64 — их и передаём как есть.set-context lab— пара «кластер — пользователь» под коротким именем.shred -u— копияadmin.confс закрытым ключом больше не нужна, ключ уже в~/.kube/config.
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kubernetes-admin@kubernetes kubernetes kubernetes-admin
lab lab lab-admin
$ kubectl --context lab cluster-info
Kubernetes control plane is running at https://k8s-lab-api.example.com:6443
CoreDNS is running at https://k8s-lab-api.example.com:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
$ kubectl config use-context lab
Switched to context "lab".
Дальше в этой и следующих частях блока команды kubectl без --context выполняются в тестовом кластере: текущий контекст — lab. Помнить о текущем контексте приходится постоянно: он записан в файле и общий для всех окон терминала, и kubectl delete в «не том» кластере — классическая ошибка. Поэтому в скриптах контекст указывают явно (kubectl --context lab …, у Helm — --kube-context lab), а в приглашение командной строки многие выводят имя текущего контекста.
Calico сразу для Windows
Узел пока NotReady — сети подов нет, как и в части 4 до установки Calico. Устанавливаем Calico тем же оператором, но сразу со всем, что понадобится Windows-узлу: никакого VXLANCrossSubnet, никакого BGP и никакой миграции потом.
CRD и оператор
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/v3_projectcalico_org.yaml
customresourcedefinition.apiextensions.k8s.io/apiservers.operator.tigera.io created
...
customresourcedefinition.apiextensions.k8s.io/ipamconfigurations.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.projectcalico.org created
...
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/tigera-operator.yaml
namespace/tigera-operator created
...
deployment.apps/tigera-operator created
Отличие от части 4 — в файле CRD. Там ресурсы Calico (IPPool, NetworkPolicy Calico и другие) хранились в служебных CRD группы crd.projectcalico.org, а «настоящий» API projectcalico.org/v3 с проверкой и значениями по умолчанию отдавал отдельный агрегированный API-сервер Calico. Начиная с Calico 3.33 новые установки на Kubernetes 1.36 и новее по умолчанию используют нативные CRD projectcalico.org/v3: ресурсы Calico — обычные CRD, kubectl работает с ними напрямую, а проверку и значения по умолчанию делают политики допуска (MutatingAdmissionPolicy и ValidatingAdmissionPolicy — правила на языке CEL, которые kube-apiserver исполняет сам, без вызова внешнего вебхука) и небольшой под вебхуков. Для Kubernetes 1.34–1.35 у Calico есть отдельный файл CRD, а kubectl create вместо apply нужен по той же причине, что в части 4: CRD слишком велики для аннотации клиентского apply.
Адрес API для Calico
До установки Calico создаём ConfigMap, которым оператор передаёт компонентам Calico адрес API:
kind: ConfigMap
apiVersion: v1
metadata:
name: kubernetes-services-endpoint
namespace: tigera-operator
data:
KUBERNETES_SERVICE_HOST: k8s-lab-api.example.com
KUBERNETES_SERVICE_PORT: "6443"
$ kubectl apply -f kubernetes-services-endpoint.yaml
configmap/kubernetes-services-endpoint created
Обычно поды ходят в API по адресу сервиса kubernetes — 10.96.0.1. На Windows-узле это не работает: Calico запускается в HostProcess-поде в сети хоста, а Windows-хост к адресам сервисов обращаться не может — их балансировку VFP делает на портах подов, а не на самом хосте. К тому же calico-node-windows запускается раньше, чем kube-proxy настроит балансировку сервисов: kube-proxy для Windows ждёт сеть, которую создаёт Calico, — без прямого адреса API Calico не запустился бы никогда. Поэтому оператор передаёт компонентам Calico переменные KUBERNETES_SERVICE_HOST и KUBERNETES_SERVICE_PORT из этого ConfigMap, и они идут к API напрямую — по общему имени. ConfigMap действует и на Linux-компоненты: на Linux-узлах calico-node тоже ходит к k8s-lab-api.example.com, а не через kube-proxy, и не зависит от него при старте. В основном кластере ConfigMap пришлось бы добавлять задним числом, с перезапуском всех calico-node; здесь он есть с первой минуты.
Installation
calico-installation.yaml:
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
serviceCIDRs:
- 10.96.0.0/12
calicoNetwork:
bgp: Disabled
linuxDataplane: Nftables
windowsDataplane: HNS
ipPools:
- name: default-ipv4-ippool
cidr: 172.21.0.0/16
blockSize: 26
encapsulation: VXLAN
natOutgoing: Enabled
nodeSelector: all()
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
name: default
spec: {}
Каждое поле здесь выбрано в прошлой части, коротко напомним:
bgp: Disabled— calico-node не запускает BIRD, маршруты к блокам других узлов и на Linux прописывает Felix по ресурсам Calico. Calico для Windows не умеет VXLAN вместе с BGP.encapsulation: VXLANвместоVXLANCrossSubnet— пакеты между подами разных узлов всегда идут в туннеле VXLAN, даже в пределах одной подсети: L2-кадр пода заворачивается в UDP-датаграмму на порт 4789 между адресами узлов (часть 18 сетевой серии). Цена — 50 байт заголовков на пакет и MTU подов 1450 вместо 1500. VNI 4096 и порт 4789 — значения по умолчанию, и других Windows не примет.windowsDataplane: HNS— единственный датаплейн Calico для Windows. Пока поле не задано, оператор Windows-узлы не обслуживает, а с ним создаёт DaemonSetcalico-node-windows— сейчас пустой, Windows-узлов нет.serviceCIDRs— сеть сервисов, совпадающая сserviceSubnetиз конфигурации kubeadm: CNI-плагин на Windows вешает на конечную точку HNS пода политику исходящего NAT (OutBoundNAT) с исключениями — сеть подов и сеть сервисов, — чтобы трафик к подам и сервисам уходил с адресом пода.cidr: 172.21.0.0/16,blockSize: 26,natOutgoing: Enabled— как в части 4, только своя сеть; блок /26 на Windows-узле — это 60 адресов для подов, об этом в разделе проkubeadm join.linuxDataplane: NftablesиAPIServer— как в части 4. С нативными CRDAPIServerразворачивает не агрегированный API-сервер, а под вебхуков проверки.
$ kubectl create -f calico-installation.yaml
installation.operator.tigera.io/default created
apiserver.operator.tigera.io/default created
$ kubectl get tigerastatus
NAME AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE
apiserver True False False 3m45s All objects available
calico True False False 3m10s All objects available
calico-windows True Invalid StrictAffinity, it must be set to 'true' when using Calico IPAM on Windows: StrictAffinity is false, it must be set to 'true' in the default IPAMConfiguration when using Calico IPAM on Windows
ippools True False False 4m50s All objects available
tiers True False False 4m50s All objects available
Строк здесь больше, чем в части 4: tiers — уровни политик Calico из части 20, а calico-windows — состояние Windows-части, которое оператор показывает, раз в Installation есть windowsDataplane. Она в состоянии DEGRADED и прямо пишет, чего не хватает.
strictAffinity
Последняя настройка IPAM — модуля Calico, который раздаёт адреса подам:
$ kubectl patch ipamconfigurations default --type merge --patch='{"spec": {"strictAffinity": true}}'
ipamconfiguration.projectcalico.org/default patched
$ kubectl get ipamconfigurations default -o jsonpath='{.spec.strictAffinity}{"\n"}'
true
$ kubectl get tigerastatus calico-windows
NAME AVAILABLE PROGRESSING DEGRADED SINCE
calico-windows True False False 25s
strictAffinity: true запрещает всем узлам занимать адреса из чужих блоков: Windows-узел держит ровно один блок, и адрес из него на другом узле сломал бы маршрутизацию (подробно — в прошлой части). Ставим сейчас, пока Windows-узла нет и ни один Linux-узел не успел занять чужой адрес. Ресурс IPAMConfiguration с именем default Calico создаёт сам при установке, нативный CRD позволяет менять его прямо kubectl.
Поды Calico на мастере:
$ kubectl -n calico-system get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
calico-kube-controllers-7cbbcdd58d-2h4x8 1/1 Running 0 4m50s 172.21.70.196 k8s-lab-master-1
calico-node-k74mn 1/1 Running 0 4m53s 192.168.10.23 k8s-lab-master-1
calico-typha-7c68577f9-hjzzm 1/1 Running 0 4m54s 192.168.10.23 k8s-lab-master-1
calico-webhooks-648955b74-2m5p7 1/1 Running 0 4m57s 172.21.70.195 k8s-lab-master-1
calico-webhooks-648955b74-zqrkl 1/1 Running 0 4m57s 172.21.70.194 k8s-lab-master-1
csi-node-driver-gjfgd 2/2 Running 0 4m52s 172.21.70.197 k8s-lab-master-1
calico-webhooks — тот самый под вебхуков нативных CRD вместо calico-apiserver. Пока в кластере один узел, всё работает на мастере: его taint Calico допускает.
Linux-воркеры
k8s-lab-node-1 и k8s-lab-node-2 подключаем командой из вывода init, как в части 4, раздел «Рабочие узлы: kubeadm join», и одобряем серверные CSR kubelet мастера и воркеров, сверив имена и адреса, как в разделе «Сертификат для HTTPS kubelet». Команда на обоих воркерах одна и та же, ниже — первый:
user@k8s-lab-node-1:~$ sudo kubeadm join k8s-lab-api.example.com:6443 --token 3h8q2w.k5n9v1c7x4m6z0p8 \
--discovery-token-ca-cert-hash sha256:7b2e9d4a1c6f8b3e0d5a2c7f9e4b1d6a8c3f0e5b2d7a9c4e1f6b8d3a0c5e2f7b
...
This node has joined the cluster:
...
$ kubectl get csr --field-selector spec.signerName=kubernetes.io/kubelet-serving
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
csr-4wm2t 7m2s kubernetes.io/kubelet-serving system:node:k8s-lab-master-1 <none> Pending
csr-7672m 18s kubernetes.io/kubelet-serving system:node:k8s-lab-node-1 <none> Pending
csr-gsll8 9s kubernetes.io/kubelet-serving system:node:k8s-lab-node-2 <none> Pending
csr-lp4tq 7m21s kubernetes.io/kubelet-serving system:node:k8s-lab-master-1 <none> Pending
$ kubectl certificate approve csr-4wm2t csr-7672m csr-gsll8 csr-lp4tq
certificatesigningrequest.certificates.k8s.io/csr-4wm2t approved
certificatesigningrequest.certificates.k8s.io/csr-7672m approved
certificatesigningrequest.certificates.k8s.io/csr-gsll8 approved
certificatesigningrequest.certificates.k8s.io/csr-lp4tq approved
От мастера запросов два: kubelet повторяет CSR, пока на первый нет ответа, — одобрить можно оба, после выдачи сертификата kubelet возьмёт один. Пока CSR не одобрены, серверного сертификата у kubelet нет вовсе: kubectl logs к подам узла падает с remote error: tls: internal error, а metrics-server не проходит проверку готовности.
Обе реплики CoreDNS запустились на мастере ещё до появления воркеров: других узлов не было, а taint мастера CoreDNS допускает. Сами они не переедут — планировщик не трогает работающие поды. Перезапустим Deployment:
$ kubectl -n kube-system rollout restart deployment coredns
deployment.apps/coredns restarted
$ kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
NAME READY STATUS RESTARTS AGE IP NODE
coredns-8f97fdbd9-9gc9h 1/1 Running 0 27s 172.21.172.3 k8s-lab-node-2
coredns-8f97fdbd9-vr6vv 1/1 Running 0 26s 172.21.70.199 k8s-lab-master-1
Реплики разошлись по разным узлам — это правило антиаффинности в манифесте kubeadm: «реплики kube-dns предпочтительно на разных узлах». Но одна из них снова на мастере. Правило только предпочтительное, taint мастера CoreDNS допускает, и планировщик сложил антиаффинность с остальными оценками узлов. Для DNS важно, что реплики на разных узлах: выключение любого из них оставит кластер с работающим DNS.
Проверка Linux-части
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-lab-master-1 Ready control-plane 8m2s v1.37.2 192.168.10.23 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
k8s-lab-node-1 Ready <none> 60s v1.37.2 192.168.10.24 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
k8s-lab-node-2 Ready <none> 51s v1.37.2 192.168.10.26 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
$ kubectl -n calico-system get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
calico-node 3 3 3 3 3 kubernetes.io/os=linux 6m49s
calico-node-windows 0 0 0 0 0 kubernetes.io/os=windows 107s
csi-node-driver 3 3 3 3 3 kubernetes.io/os=linux 6m48s
$ kubectl -n calico-system get pods -l k8s-app=calico-typha -o wide
NAME READY STATUS RESTARTS AGE IP NODE
calico-typha-7c68577f9-8gjrr 1/1 Running 0 50s 192.168.10.24 k8s-lab-node-1
calico-typha-7c68577f9-hjzzm 1/1 Running 0 6m51s 192.168.10.23 k8s-lab-master-1
В колонке KERNEL-VERSION kubectl 1.37 дописывает к версии ядра архитектуру узла. calico-node-windows уже есть, с нулём желаемых подов — его создал windowsDataplane: HNS. Typha — посредник между API и агентами Calico на узлах: он один держит watch к kube-apiserver и раздаёт изменения всем Felix, чтобы сотни узлов не нагружали API каждый своим watch. Число реплик оператор выбирает по числу узлов: пока узел был один, работала одна реплика, с тремя узлами — две, на разных узлах, в сети узла (адрес пода — адрес узла). Felix на Windows-узле тоже будет подключаться к одной из них.
Главное отличие от основного кластера видно в таблице маршрутов Linux-воркера:
user@k8s-lab-node-2:~$ ip route
default via 192.168.10.1 dev ens18 proto static
172.21.70.192/26 via 172.21.70.192 dev vxlan.calico onlink mtu 1450
blackhole 172.21.172.0/26 proto 80
172.21.172.2 dev calid60012519f0 scope link metric 1024
172.21.172.3 dev caliad8a24266f7 scope link metric 1024
172.21.211.0/26 via 172.21.211.0 dev vxlan.calico onlink mtu 1450
192.168.10.0/24 dev ens18 proto kernel scope link src 192.168.10.26
user@k8s-lab-node-2:~$ ip -d link show vxlan.calico | grep -o 'vxlan id [0-9]* .*dstport [0-9]*'
vxlan id 4096 local 192.168.10.26 dev ens18 srcport 0 0 dstport 4789
user@k8s-lab-node-2:~$ pgrep -a bird || echo 'BIRD не запущен'
BIRD не запущен
user@k8s-lab-node-2:~$ sudo ss -ltn 'sport = :179'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
В части 4 маршрут к блоку соседа шёл via 192.168.10.17 dev ens18 proto 80 — напрямую через адрес узла. Здесь блоки мастера 172.21.70.192/26 и первого воркера 172.21.211.0/26 доступны через интерфейс vxlan.calico с туннельными адресами этих узлов (первый адрес блока — 172.21.70.192, 172.21.211.0 — Calico отдаёт VXLAN-интерфейсу узла), а у маршрутов свой MTU 1450 — место под заголовки VXLAN. Метки proto 80 у таких маршрутов нет: Felix ставит её только на blackhole собственного блока. onlink говорит ядру считать шлюз доступным прямо через этот интерфейс, хотя он и не в подсети интерфейса. VNI 4096, порт 4789, BGP-порт 179 никто не слушает. Блок самого воркера — 172.21.172.0/26, его адрес .0 занят туннельным адресом, .2 и .3 — поды Calico (csi-node-driver) и CoreDNS.
metrics-server и cert-manager
Два дополнения нужны тестовому кластеру сразу.
metrics-server — как в части 32, раздел «metrics-server», тем же манифестом и без --kubelet-insecure-tls: серверные сертификаты kubelet подписаны CA тестового кластера, CSR мы только что одобрили. Если забыть их одобрить, под metrics-server будет Running, но не Ready: проверка готовности отвечает 500, пока он не сможет снять метрики ни с одного kubelet.
$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml
...
$ kubectl get apiservice v1beta1.metrics.k8s.io
NAME SERVICE AVAILABLE AGE
v1beta1.metrics.k8s.io kube-system/metrics-server True 2m35s
cert-manager — как в части 19, раздел «Установка», тем же манифестом, но без флага --enable-gateway-api: Gateway API и Traefik в тестовом кластере не нужны. Издатели Let's Encrypt тоже не нужны — cert-manager здесь ради вебхука gMSA из части 40, которому нужен сертификат для HTTPS, выпущенный внутри кластера.
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.2/cert-manager.yaml
...
$ kubectl -n cert-manager rollout status deploy cert-manager-webhook
deployment "cert-manager-webhook" successfully rolled out
Оба манифеста задают своим Deployment nodeSelector: kubernetes.io/os: linux — Windows-узел их не заинтересует, даже если когда-нибудь лишится taint. Prometheus, Grafana и csi-driver-smb поставим в тестовый кластер в части 39, а Traefik, MetalLB и остальное из основного кластера ему не понадобятся.
hello для проверки
Для проверки связности между ОС нужен Linux-сервис. Возьмём hello из части 5 — из того же частного пакета в ghcr.io, с секретом ghcr-pull для скачивания, привязанным к ServiceAccount default неймспейса, как в той части:
$ kubectl create namespace demo
$ kubectl label namespace demo pod-security.kubernetes.io/enforce=restricted
$ read -rsp 'GitHub token (read:packages): ' GHCR_TOKEN; echo
$ kubectl -n demo create secret docker-registry ghcr-pull \
--docker-server=ghcr.io --docker-username=example --docker-password="$GHCR_TOKEN"
$ unset GHCR_TOKEN
$ kubectl -n demo patch serviceaccount default -p '{"imagePullSecrets":[{"name":"ghcr-pull"}]}'
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: demo
labels:
app.kubernetes.io/name: hello
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: hello
template:
metadata:
labels:
app.kubernetes.io/name: hello
spec:
os:
name: linux
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: hello
image: ghcr.io/example/hello:1.2
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet: {path: /readyz, port: http}
livenessProbe:
httpGet: {path: /healthz, port: http}
resources:
requests: {cpu: 100m, memory: 32Mi}
limits: {memory: 64Mi}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: hello
namespace: demo
spec:
selector:
app.kubernetes.io/name: hello
ports:
- name: http
port: 80
targetPort: http
$ kubectl apply -f hello.yaml
deployment.apps/hello created
service/hello created
$ kubectl -n demo get pods,svc -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/hello-55fb8f8684-69mvv 1/1 Running 0 45s 172.21.211.25 k8s-lab-node-1 <none> <none>
pod/hello-55fb8f8684-nztv2 1/1 Running 0 45s 172.21.172.4 k8s-lab-node-2 <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/hello ClusterIP 10.97.141.163 <none> 80/TCP 45s app.kubernetes.io/name=hello
Реплики разошлись по двум Linux-воркерам: мастер закрыт taint control-plane, а планировщик по умолчанию разносит поды одного Deployment по разным узлам. Linux-основа готова — дальше Windows.
Подготовка Windows Server 2025 Core
Действующие лица из двух прошлых частей, коротко:
- SCM (Service Control Manager) — диспетчер служб Windows, в этой роли аналог systemd: запускает службы при загрузке, перезапускает, учитывает зависимости.
- HCS (Host Compute Service, служба
vmcompute) — API Windows для создания контейнеров. Контейнер с изоляцией процессов — это job object (группа процессов с общими ограничениями памяти и процессора — роль cgroups), server silo (job object с собственными пространствами имён объектов, реестра и файлов — роль namespaces) и файловая система из слоёв образа. - HNS (Host Networking Service, служба
hns) — создаёт виртуальные сети и конечные точки (endpoints, сетевые карты контейнеров) на виртуальном коммутаторе Hyper-V (vSwitch) и вешает на них политики; VFP (Virtual Filtering Platform) — расширение vSwitch, которое исполняет политики на каждом порту: NAT, ACL, балансировку, инкапсуляцию. Это место netfilter. - HostProcess-контейнер — обычный процесс хоста внутри job object, без изоляции файловой системы и сети. Так на Windows запускают сетевой плагин и kube-proxy, для которых на Linux берут привилегированные поды.
Вот что окажется на узле к концу статьи:
k8s-lab-win-1.example.com — 192.168.10.25, Windows Server 2025 Core
┌───────────────────────────────────────────────────────────────┐
│ Службы Windows (SCM) │
│ containerd ◄──── CRI, именованный канал ──── kubelet (nssm) │
│ │ │
│ ▼ containerd-shim-runhcs-v1 │
│ HCS: job object, слои образа; сеть контейнера — через HNS │
│ │
│ HostProcess-поды (процессы хоста в сети хоста) │
│ calico-node-windows: node, felix ──► HNS: сеть, ACL │
│ kube-proxy-windows: winkernel ──► HNS: балансировщики │
│ │
│ HNS + vSwitch с расширением VFP │
│ сеть «Calico»: Overlay, 172.21.41.0/26, VNI 4096 │
│ шлюз 172.21.41.1 — он же туннельный адрес узла │
│ Calico_ep 172.21.41.2 — адрес хоста в сети подов │
│ поды 172.21.41.5, .6 — aspnetapp │
│ vEthernet (Ethernet) 192.168.10.25 — адрес самого хоста │
└───────────────────────────────┬───────────────────────────────┘
│ Ethernet; VXLAN — UDP 4789
▼
сеть узлов 192.168.10.0/24
Установку containerd, kubelet и kubeadm берут на себя два PowerShell-скрипта из репозитория sig-windows-tools группы SIG Windows; на них опирается и официальное руководство Adding Windows worker nodes. Скрипты делают немного, и ниже разобрано всё, что они меняют.
Всё в этом разделе выполняется на k8s-lab-win-1 в PowerShell от имени администратора. Server Core после входа открывает sconfig — текстовое меню основных настроек; всё, что в нём есть, доступно и командами, а команды можно повторить на следующем узле.
Имя и адрес
PS C:\> New-NetIPAddress -InterfaceAlias Ethernet -IPAddress 192.168.10.25 -PrefixLength 24 -DefaultGateway 192.168.10.1
PS C:\> Set-DnsClientServerAddress -InterfaceAlias Ethernet -ServerAddresses 192.168.10.1
PS C:\> Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'NV Domain' -Value 'example.com'
PS C:\> Rename-Computer -NewName k8s-lab-win-1 -Restart
...
PS C:\> [System.Net.Dns]::GetHostEntry('').HostName
k8s-lab-win-1.example.com
PS C:\> (Resolve-DnsName k8s-lab-api.example.com -Type A).IPAddress
192.168.10.23
Имя узла в кластере берётся из имени компьютера, поэтому оно должно быть уникальным и стабильным; NetBIOS-имя компьютера ограничено 15 символами, и k8s-lab-win-1 в него укладывается. NV Domain — основной DNS-суффикс: узел не входит в домен Active Directory (нужен ли он Windows-узлу — вопрос части 40), и без суффикса его полное имя было бы просто k8s-lab-win-1. Адреса и DNS из PowerShell подробно разобраны во второй части серии про Windows Server. Шаблон виртуальной машины для клонирования готовят командой sysprep /generalize, которая сбрасывает идентификаторы установки, — аналог очистки /etc/machine-id.
Обновления
На Windows обновления ОС влияют на то, какие образы можно запускать: контейнер с изоляцией процессов работает на ядре хоста, и сборка ОС в образе должна совпадать со сборкой хоста — 10.0.26100 для Windows Server 2025 и тегов ltsc2025. Номер ежемесячного обновления (UBR) совпадать не обязан, но Microsoft тестирует новые ревизии образов на обновлённых хостах, поэтому хост обновляют до установки компонентов (пункт «Install updates» в sconfig) и затем каждый месяц:
PS C:\> Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' |
Select-Object ProductName, InstallationType, CurrentBuild, UBR
ProductName InstallationType CurrentBuild UBR
----------- ---------------- ------------ ---
Windows Server 2025 Datacenter Server Core 26100 33438
На свежеустановленном с ISO сервере обновления могут встать не с первого раза. Накопительное обновление требует свежего стека обслуживания (servicing stack — компонента, который сам ставит обновления), и если он устанавливается в том же проходе, накопительное завершается ошибкой 0x800F0823; после перезагрузки установку запускают ещё раз, и тогда оно встаёт. Само накопительное обновление ставится десятки минут и перезагружает сервер дважды.
Режим обновлений в sconfig переключают в Manual: узел кластера перезагружают только через drain и uncordon, как в части 35. Обновление работающего Windows-узла — тема части 39.
Время
Время на Windows синхронизирует служба W32Time — клиент и сервер NTP, протокола сверки часов с источником точного времени. В рабочей группе она по умолчанию ходит к time.windows.com; направим её к router.example.com, как Linux-узлы:
PS C:\> w32tm /config /manualpeerlist:"router.example.com,0x8" /syncfromflags:manual /update
The command completed successfully.
PS C:\> Restart-Service w32time; w32tm /resync
The command completed successfully.
PS C:\> w32tm /query /status | Select-String 'Stratum|Source'
Stratum: 3 (secondary reference - syncd by (S)NTP)
Source: router.example.com,0x8
Флаг 0x8 — режим клиента. Узел в домене получал бы время по доменной иерархии (часть 12 серии про Windows Server). Последствия ушедших часов те же, что на Linux: сертификаты kubelet «ещё не действительны» или «уже истекли».
Компонент Containers
PS C:\> Install-WindowsFeature -Name Containers
Success Restart Needed Exit Code Feature Result
------- -------------- --------- --------------
True Yes SuccessRest... {Containers}
WARNING: You must restart this server to finish the installation process.
PS C:\> Restart-Computer
Компонент Containers — место модулей overlay и br_netfilter из части 4: он приносит службы HCS (vmcompute) и HNS (hns), фильтр-драйверы файловой системы контейнеров и виртуальный коммутатор; ip_forward не нужен — пересылку делает vSwitch.
Файрвол
Windows Defender Firewall, в отличие от ufw в Ubuntu Server, включён по умолчанию. Узлу нужны 10250/TCP для API kubelet (правило создаст PrepareNode.ps1) и 4789/UDP для VXLAN. Правило для VXLAN создавать руками не нужно: когда Calico создаст на узле overlay-сеть, HNS добавит входящее правило OverlayTraffic4789UDP («Overlay Traffic 4789 UDP») сам — мы увидим его ниже. Остаётся по желанию 10256/TCP для проверки здоровья kube-proxy, которой пользуются внешние балансировщики:
PS C:\> New-NetFirewallRule -Name kube-proxy-health -DisplayName 'kube-proxy health' -Direction Inbound `
-Protocol TCP -LocalPort 10256 -RemoteAddress 192.168.10.0/24 -Action Allow
Правило kubelet скрипт создаст открытым для всех адресов, его стоит сузить так же: Set-NetFirewallRule -Name kubelet -RemoteAddress 192.168.10.0/24.
containerd
PS C:\> curl.exe -LO https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/master/hostprocess/Install-Containerd.ps1
PS C:\> .\Install-Containerd.ps1 -ContainerDVersion 2.2.2 -crictlVersion 1.37.0 -skipHypervisorSupportCheck
Windows feature 'Containers' is already installed.
WARNING: Windows optional feature 'Microsoft-Hyper-V' is not enabled. Enabling it now...
Deployment Image Servicing and Management tool
...
The operation completed successfully.
WARNING: Windows optional feature 'Microsoft-Hyper-V-Online' is not disabled. Disabling it now...
...
The operation completed successfully.
WARNING: The Hyper-V features was installed without checking the CPU for Hypervisor support. You may not be able to host Hyper-V isolated containers if CPU doesn't support hypervisors.
Please reboot and re-run this script.
PS C:\> Restart-Computer
Скрипт лежит в ветке master, которая меняется; правильнее скачать его по ссылке на конкретный коммит, прочитать и хранить рядом с остальными файлами установки. Версию containerd он принимает без буквы v, версию crictl стоит задать по версии кластера.
Первый запуск до containerd не доходит. Без ключа -skipHypervisorSupportCheck скрипт поставил бы роли Hyper-V целиком, вместе с проверкой процессора; с ключом он включает через DISM (средство обслуживания образа Windows) компонент Microsoft-Hyper-V, выключает Microsoft-Hyper-V-Online и просит перезагрузиться и запустить его снова. Нашей виртуальной машине вложенная виртуализация не нужна: изоляция Hyper-V Kubernetes не поддерживается, и предупреждение о процессоре можно пропустить. Второй запуск:
PS C:\> .\Install-Containerd.ps1 -ContainerDVersion 2.2.2 -crictlVersion 1.37.0 -skipHypervisorSupportCheck
Windows feature 'Containers' is already installed.
Windows optional feature 'Microsoft-Hyper-V' is already enabled.
Windows optional feature 'Microsoft-Hyper-V-Online' is already disabled.
WARNING: The Hyper-V features was installed without checking the CPU for Hypervisor support. You may not be able to host Hyper-V isolated containers if CPU doesn't support hypervisors.
Getting ContainerD binaries
Downloading https://github.com/containerd/containerd/releases/download/v2.2.2/containerd-2.2.2-windows-amd64.tar.gz to C:\Program Files\containerd\containerd.tar.gz
Configuring ContainerD CNI section: [plugins.'io.containerd.cri.v1.runtime'.cni]
Registering ContainerD as a service
Starting ContainerD service
Downloading https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.37.0/crictl-v1.37.0-windows-amd64.tar.gz to C:\Program Files\containerd\crictl.tar.gz
...
Done - please remember to add '--cri-socket "npipe:////./pipe/containerd-containerd"' to your kubeadm join command if your kubernetes version is below 1.25!
Что скрипт сделал:
- проверил компонент Containers и компоненты Hyper-V — теперь они на месте;
- распаковал containerd для Windows из релиза на GitHub в
C:\Program Files\containerdи добавил каталог в системную переменнуюPATH; - сгенерировал
config.tomlкомандойcontainerd.exe config defaultи прописал в секции CNI каталогиc:/opt/cni/binиc:/etc/cni/net.d— пути как на Linux, с буквой диска; их и ждёт Calico; - зарегистрировал containerd службой (
containerd.exe --register-service): containerd сам умеет отвечать SCM и работает от имениLocalSystem; - поставил
crictlс конфигурацией$HOME\.crictl\crictl.yamlтого пользователя, который запускал скрипт, где адрес среды —npipe://./pipe/containerd-containerd.
Перед запуском скрипт стоит открыть в отдельном окне PowerShell, а не перенаправлять его вывод в файл: tar.exe пишет список распакованных файлов в поток ошибок, и при перенаправлении *>&1 Windows PowerShell 5.1 превращает эти строки в ошибки и останавливает скрипт.
CRI на Windows работает не через unix-сокет, а через именованный канал (named pipe) \\.\pipe\containerd-containerd — объект ядра для обмена данными между процессами, адресуемый по имени. Действующая конфигурация:
PS C:\> containerd.exe config dump | Select-String "^version|default_runtime_name|runhcs-wcow-process\]|runtime_type|bin_dirs|conf_dir|sandbox ="
version = 3
sandbox = 'registry.k8s.io/pause:3.10.1'
default_runtime_name = 'runhcs-wcow-process'
runtime_type = 'io.containerd.runhcs.v1'
cni_conf_dir = ''
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runhcs-wcow-process]
runtime_type = 'io.containerd.runhcs.v1'
cni_conf_dir = ''
bin_dirs = ['c:/opt/cni/bin']
conf_dir = 'c:/etc/cni/net.d'
runhcs-wcow-process— обработчик (runtime handler) по умолчанию: WCOW — Windows Containers on Windows,process— изоляция процессов. Он запускает shimcontainerd-shim-runhcs-v1.exeиз проекта hcsshim — посредника между containerd и HCS, который делает на Windows работу runc. Это имя понадобится в RuntimeClass. Рядом в конфигурации есть и обработчикrunhcs-wcow-hypervisorдля изоляции Hyper-V, которой мы не пользуемся.- Никакого
SystemdCgroup: cgroups нет, ресурсы контейнера ограничивает job object. sandbox— образ pause-контейнера.registry.k8s.io/pauseмультиплатформенный, в его списке манифестов есть варианты для Windows, и containerd выбирает совпадающий со сборкой хоста.
PS C:\> crictl version | Select-String Runtime
RuntimeName: containerd
RuntimeVersion: v2.2.2
RuntimeApiVersion: v1
kubelet и kubeadm службами Windows
PS C:\> curl.exe -LO https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/master/hostprocess/PrepareNode.ps1
PS C:\> .\PrepareNode.ps1 -KubernetesVersion v1.37.2
Using Kubernetes version: v1.37.2
Downloading https://dl.k8s.io/v1.37.2/bin/windows/amd64/kubelet.exe to C:\k\kubelet.exe
Downloading https://dl.k8s.io/v1.37.2/bin/windows/amd64/kubeadm.exe to C:\k\kubeadm.exe
...
Installing nssm
Registering kubelet service
...
Здесь версию принято писать с буквой v, хотя скрипт допишет её и сам. Пакетов и pkgs.k8s.io для Windows нет; вместо apt-get install скрипт:
- проверяет, что канал containerd на месте;
- скачивает
kubelet.exeиkubeadm.exeсdl.k8s.io/v1.37.2/bin/windows/amd64/вC:\kи добавляетC:\kвPATH; - создаёт
C:\var\log\kubelet,C:\etc\kubernetes\pki,C:\etc\kubernetes\manifestsиC:\var\lib\kubelet\etc\kubernetesс символической ссылкойpkiнаC:\etc\kubernetes\pki: пути вида/var/lib/kubeletв конфигурации kubelet и kubeadm на Windows означают эти каталоги; - пишет обёртку
C:\k\StartKubelet.ps1, которая читаетC:\var\lib\kubelet\kubeadm-flags.envи запускаетkubelet.exeс этими флагами плюс постоянными; - ставит nssm и регистрирует через него службу
kubelet, зависящую отcontainerd, с выводом вC:\var\log\kubelet\kubelet.out.logиkubelet.err.logи ротацией этих файлов раз в сутки или по 10 МБ; - создаёт правило файрвола
kubeletдля 10250/TCP.
nssm (Non-Sucking Service Manager) превращает в службу Windows любую консольную программу: отвечает SCM вместо неё, перезапускает её при падении и пишет её вывод в файлы. Служба nssm и StartKubelet.ps1 играют роль юнита kubelet.service с drop-in 10-kubeadm.conf из части 4: обёртка так же подмешивает флаги из kubeadm-flags.env. Из постоянных флагов интересны --cgroups-per-qos=false и --enforce-node-allocatable="" — иерархии cgroups нет, и kubelet её не строит (поэтому systemReserved на Windows лишь уменьшает объём, доступный планировщику), --resolv-conf="" — DNS-настройки подам kubelet передаёт прямо в конечную точку HNS, и --hostname-override с именем компьютера.
Обёртка — не единственный способ: с флагом --windows-service kubelet.exe сам отвечает SCM, и его можно зарегистрировать командой New-Service. Но тогда флаги из kubeadm-flags.env придётся переносить в параметры службы руками после каждого join и upgrade, а обёртка перечитывает их при каждом запуске. Мы следуем официальному руководству — nssm. apt-mark hold не нужен: бинарники в C:\k никто, кроме нас, не обновит.
В отличие от Linux, где kubelet до join перезапускается по кругу, здесь служба зарегистрирована, но не запущена — её запустит сам kubeadm join:
PS C:\> Get-Service kubelet | Format-Table Status, Name, StartType
Status Name StartType
------ ---- ---------
Stopped kubelet Automatic
kubeadm join
Для Windows-узла команды из вывода init мало: узел должен появиться в кластере сразу с taint, иначе между регистрацией и kubectl taint планировщик успеет отправить на него Linux-поды, которые не запустятся (у Linux-образа нет варианта для Windows). Поэтому — файл конфигурации, как kubeadm-config.yaml в части 4. Токен создаём на мастере с коротким сроком жизни:
user@k8s-lab-master-1:~$ sudo kubeadm token create --ttl 1h --description 'join k8s-lab-win-1'
6w1r8t.p3k9m2x7c5v0b4n1
На узле — C:\k\join.yaml:
apiVersion: kubeadm.k8s.io/v1beta4
kind: JoinConfiguration
discovery:
bootstrapToken:
apiServerEndpoint: k8s-lab-api.example.com:6443
token: 6w1r8t.p3k9m2x7c5v0b4n1
caCertHashes:
- sha256:7b2e9d4a1c6f8b3e0d5a2c7f9e4b1d6a8c3f0e5b2d7a9c4e1f6b8d3a0c5e2f7b
nodeRegistration:
name: k8s-lab-win-1
criSocket: npipe:////./pipe/containerd-containerd
taints:
- key: os
value: windows
effect: NoSchedule
patches:
directory: C:\k\patches
и патч C:\k\patches\kubeletconfiguration.yaml:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
maxPods: 60
discovery.bootstrapToken— то же, что--tokenи--discovery-token-ca-cert-hashв части 4: токен для TLS bootstrapping и хеш открытого ключа CA, по которому узел проверяет подлинность кластера. Хеш — тот же, что в выводеinitтестового кластера.criSocket— адрес CRI: схемаnpipe://и путь к каналу. kubeadm на Windows подставляет его и сам, но явное значение читается лучше.taints— kubelet зарегистрирует Node сразу с запретомos=windows:NoSchedule. Ключ и значение — договорённость из документации Kubernetes, а не встроенное имя.patches— каталог с патчами к конфигурации из кластера; имя файла задаёт цель,kubeletconfiguration— общийKubeletConfigurationиз ConfigMapkubelet-config. Патч действует только на этом узле.
Зачем maxPods: 60: Calico на Windows держит на узле один блок адресов и резервирует в нём четыре адреса (адрес сети, шлюз, Calico_ep и последний адрес), как разобрано в прошлой части. Из 64 адресов блока /26 подам остаётся 60, а kubelet по умолчанию рассчитывает на 110 подов — шестьдесят первый получил бы узел, но не адрес. Патчи kubeadm в кластере не хранит, поэтому при обновлении узла каталог передают снова: kubeadm upgrade node --patches C:\k\patches.
PS C:\k> kubeadm join --config C:\k\join.yaml
[preflight] Running pre-flight checks
[preflight] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[preflight] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
W1106 23:16:41.134109 5016 utils.go:69] The recommended value for "authentication.x509.clientCAFile" in "KubeletConfiguration" is: \etc\kubernetes\pki\ca.crt; the provided value is: /etc/kubernetes/pki/ca.crt
[kubelet-start] Writing kubelet configuration to file "\\var\\lib\\kubelet\\instance-config.yaml"
[patches] Reading patches from path "C:\\k\\patches"
[patches] Found the following patch files: [kubeletconfiguration.yaml]
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[kubelet-start] Writing kubelet configuration to file "\\var\\lib\\kubelet\\config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "\\var\\lib\\kubelet\\kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[kubelet-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
[kubelet-check] The kubelet is healthy after 3.0008224s
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
PS C:\k> Remove-Item C:\k\join.yaml
Механизм тот же, что в части 4: анонимное чтение cluster-info с проверкой подписи JWS и хеша CA, временный bootstrap-kubelet.conf с токеном, CSR на клиентский сертификат system:node:k8s-lab-win-1, постоянный C:\etc\kubernetes\kubelet.conf. Только kubelet kubeadm перезапускает через SCM, а не через systemd. Предупреждение про clientCAFile безобидно: kubeadm сравнивает строки путей, а /etc/kubernetes/pki/ca.crt на Windows и так означает C:\etc\kubernetes\pki\ca.crt. Патч применён трижды — к общей конфигурации, к config.yaml узла и к его instance-config.yaml, куда kubeadm с недавних пор кладёт параметры конкретного узла. join.yaml с токеном удаляем сразу.
Серверный сертификат kubelet (serverTLSBootstrap: true из общей конфигурации) ждёт одобрения, как у Linux-узлов. Сверяем имена и одобряем:
$ kubectl get csr --field-selector spec.signerName=kubernetes.io/kubelet-serving | grep Pending
csr-ljq25 9s kubernetes.io/kubelet-serving system:node:k8s-lab-win-1 <none> Pending
$ kubectl get csr csr-ljq25 -o jsonpath='{.spec.request}' | base64 -d | openssl req -noout -text | grep -A1 'Subject Alternative'
X509v3 Subject Alternative Name:
DNS:k8s-lab-win-1, IP Address:192.168.10.25
$ kubectl certificate approve csr-ljq25
certificatesigningrequest.certificates.k8s.io/csr-ljq25 approved
Узел в кластере, но, как мастер до установки Calico, — NotReady:
$ kubectl get node k8s-lab-win-1 -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-lab-win-1 NotReady <none> 10s v1.37.2 192.168.10.25 <none> Windows Server 2025 Datacenter 10.0.26100.33438 (amd64) containerd://2.2.2
$ kubectl describe node k8s-lab-win-1 | grep -E 'NetworkReady'
Ready False Fri, 06 Nov 2026 23:16:46 +0300 Fri, 06 Nov 2026 23:16:46 +0300 KubeletNotReady [container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized, CSINode is not yet initialized]
$ kubectl get node k8s-lab-win-1 -o jsonpath='{.spec.taints}{"\n"}{.status.allocatable.pods}{"\n"}'
[{"effect":"NoSchedule","key":"os","value":"windows"},{"effect":"NoSchedule","key":"node.kubernetes.io/not-ready"},{"effect":"NoExecute","key":"node.kubernetes.io/not-ready","timeAdded":"2026-11-06T20:16:50Z"}]
60
Taint os=windows:NoSchedule стоит с первой секунды, рядом — служебные not-ready, которые снимутся сами. Ёмкость узла — 60 подов, патч сработал.
Calico для Windows
Что уже готово в кластере
В основном кластере здесь пришлось бы менять Installation и создавать ConfigMap. Тестовый кластер мы собрали сразу под Windows, поэтому только проверяем:
$ kubectl get installation default -o jsonpath='{.spec.calicoNetwork.bgp} {.spec.calicoNetwork.ipPools[0].encapsulation} {.spec.calicoNetwork.windowsDataplane} {.spec.serviceCIDRs}{"\n"}'
Disabled VXLAN HNS ["10.96.0.0/12"]
$ kubectl get ipamconfigurations default -o jsonpath='{.spec.strictAffinity}{"\n"}'
true
$ kubectl -n tigera-operator get configmap kubernetes-services-endpoint -o jsonpath='{.data}{"\n"}'
{"KUBERNETES_SERVICE_HOST":"k8s-lab-api.example.com","KUBERNETES_SERVICE_PORT":"6443"}
Все условия Calico для Windows на месте: VXLAN без CrossSubnet, BGP выключен, датаплейн HNS включён, сеть сервисов известна, strictAffinity и прямой адрес API. Ничего больше делать не нужно: как только в кластере появился узел с меткой kubernetes.io/os=windows, DaemonSet calico-node-windows получил желаемый под.
Про версию ОС. Страница требований Calico для Windows на момент написания перечисляет Windows Server 1809 и 2022, а Windows Server 2025 в ней нет; там же сказано, что поддерживаемые версии зависят от версии Kubernetes, со ссылкой на таблицу Kubernetes, где 2025 есть. У нас связка Calico 3.33.0 и Windows Server 2025 (26100.33438) работает, но формально Calico её явно не подтверждает. Для рабочего кластера это повод сверить release notes своей версии Calico или держать Windows-узлы на Windows Server 2022, пока 2025 не появится в списке.
Что появляется на узле
$ kubectl -n calico-system get ds calico-node-windows
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
calico-node-windows 1 1 0 1 0 kubernetes.io/os=windows 83m
$ kubectl -n calico-system get pod -l k8s-app=calico-node-windows -o json \
| jq -c '.items[0].spec | {init: [.initContainers[].name], containers: [.containers[] | {name, windowsOptions: .securityContext.windowsOptions}]}'
{"init":["uninstall-calico","install-cni"],"containers":[{"name":"felix","windowsOptions":{"hostProcess":true,"runAsUserName":"NT AUTHORITY\\system"}},{"name":"node","windowsOptions":{"hostProcess":true,"runAsUserName":"NT AUTHORITY\\system"}}]}
Разберём под:
hostProcess: trueиrunAsUserName: NT AUTHORITY\systemзаданы у каждого контейнера, а не у пода, — так тоже можно: контейнеры работают как процессы хоста от имени SYSTEM, самой привилегированной локальной учётной записи, в сети хоста (hostNetwork: true, у пода адрес узла). HCS всё равно создаёт для них job object (так kubelet считает ресурсы и останавливает под целиком), а файловую систему образа подключает в каталог, путь к которому лежит в переменнойCONTAINER_SANDBOX_MOUNT_POINT. Процесс видит диски, реестр, службы и сеть хоста, поэтому такие поды запрещены уровнямиbaselineиrestrictedPod Security Admission (часть 30), и неймспейсcalico-systemоператор помечает уровнемprivileged.uninstall-calicoудаляет Calico, поставленный прежним способом (скриптами и службами Windows);install-cniкладёт плагины вC:\opt\cni\bin, а конфигурацию CNI и kubeconfig плагина — вC:\etc\cni\net.d.nodeрегистрирует узел в данных Calico, получает блок адресов и создаёт HNS-сеть.felixподключается к Typha на Linux-узлах, превращает NetworkPolicy (часть 20) в ACL-политики на конечных точках HNS, которые исполняет VFP, и прописывает в HNS-сеть маршруты к блокам подов других узлов через VXLAN. Политик для самих узлов (host endpoints) на Windows нет.
В режиме BGP был бы ещё контейнер confd и роль RemoteAccess на узле. Под готов через три-четыре минуты:
$ kubectl -n calico-system describe pod -l k8s-app=calico-node-windows | grep -E 'Pulled|Unhealthy'
Normal Pulled 116s kubelet spec.initContainers{uninstall-calico}: Successfully pulled image "quay.io/calico/node-windows:v3.33.0" in 1m0.951s (1m0.951s including waiting). Image size: 146560272 bytes.
Normal Pulled 92s kubelet spec.initContainers{install-cni}: Successfully pulled image "quay.io/calico/cni-windows:v3.33.0" in 17.528s (17.528s including waiting). Image size: 149137842 bytes.
Warning Unhealthy 20s (x5 over 58s) kubelet spec.containers{felix}: Liveness probe failed: calico/node is not ready: Felix is not live: Get "http://localhost:9099/liveness": dial tcp [::1]:9099: connectex: No connection could be made because the target machine actively refused it.
$ kubectl -n calico-system get pods -l k8s-app=calico-node-windows
NAME READY STATUS RESTARTS AGE
calico-node-windows-q54h2 2/2 Running 0 4m6s
$ kubectl get node k8s-lab-win-1
NAME STATUS ROLES AGE VERSION
k8s-lab-win-1 Ready <none> 4m6s v1.37.2
Образы calico/node-windows и calico/cni-windows весят по 140 МБ с лишним. Первую минуту после старта Felix не отвечает на пробы — это нормально, пока он строит сеть, и под не перезапускается: kubelet ждёт, пока проба живучести не провалится нужное число раз подряд.
На узле появились HNS-сети и новый сетевой адаптер:
PS C:\> Get-HnsNetwork | Select-Object Name, Type,
@{n='Subnet';e={$_.Subnets.AddressPrefix}}, @{n='Gateway';e={$_.Subnets.GatewayAddress}}, ManagementIP |
Format-Table
Name Type Subnet Gateway ManagementIP
---- ---- ------ ------- ------------
External Overlay 192.168.255.0/30 192.168.255.1 192.168.10.25
Calico Overlay 172.21.41.0/26 172.21.41.1 192.168.10.25
PS C:\> Get-NetIPAddress -AddressFamily IPv4 | Where-Object PrefixOrigin -ne WellKnown |
Format-Table InterfaceAlias, IPAddress, PrefixLength
InterfaceAlias IPAddress PrefixLength
-------------- --------- ------------
vEthernet (Ethernet) 192.168.10.25 24
PS C:\> Get-HnsEndpoint | Where-Object Name -eq Calico_ep | Format-List Name, IPAddress, VirtualNetworkName
Name : Calico_ep
IPAddress : 172.21.41.2
VirtualNetworkName : Calico
PS C:\> Get-NetFirewallRule -Name OverlayTraffic4789UDP | Format-Table Name, DisplayName, Enabled, Direction, Action
Name DisplayName Enabled Direction Action
---- ----------- ------- --------- ------
OverlayTraffic4789UDP Overlay Traffic 4789 UDP True Inbound Allow
- Сеть
External— служебная сеть типа Overlay с крошечной подсетью192.168.255.0/30: Calico создаёт её первой, и вместе с ней HNS поднимает внешний vSwitch поверх адаптераEthernet. - Сеть
CalicoтипаOverlay— с подсетью-блоком172.21.41.0/26, который Calico выделил узлу. Шлюз сети172.21.41.1— порт распределённого маршрутизатора vSwitch; его же Calico записывает узлу как туннельный адрес VXLAN. В режиме Overlay трафик к подам других узлов VFP заворачивает в VXLAN. vEthernet (Ethernet)— создавая первую сеть, HNS перенёс адрес хоста на виртуальный адаптер в коммутаторе. В момент переноса сеть хоста на несколько секунд пропадает, и kubelet иногда не восстанавливает соединение с API сам (см. раздел о диагностике).Calico_ep— конечная точка самого узла в сети подов: через него хост достаёт до подов, а kube-proxy использует его адрес как source VIP.OverlayTraffic4789UDP— то самое правило файрвола для VXLAN, которое HNS добавил вместе с overlay-сетью.
kube-proxy для Windows
DaemonSet kube-proxy от kubeadm на Windows-узел не попадёт: у него nodeSelector: kubernetes.io/os: linux, и образ у него только для Linux. Для Windows нужен отдельный DaemonSet в HostProcess-подах; манифест для Calico лежит в sig-windows-tools, заглушку KUBE_PROXY_VERSION заменяем на версию кластера:
$ curl -sL https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/master/hostprocess/calico/kube-proxy/kube-proxy.yml \
| sed 's/KUBE_PROXY_VERSION/v1.37.2/g' > kube-proxy-windows.yaml
$ grep -E 'image:|hostProcess:|kubernetes.io/os' kube-proxy-windows.yaml
hostProcess: true
- image: sigwindowstools/kube-proxy:v1.37.2-calico-hostprocess
kubernetes.io/os: windows
$ kubectl apply -f kube-proxy-windows.yaml
daemonset.apps/kube-proxy-windows created
Манифест создаёт только DaemonSet kube-proxy-windows в kube-system; сервисную учётную запись kube-proxy с правами RBAC и ConfigMap kube-proxy с kubeconfig он берёт у Linux-собрата. config.conf из того же ConfigMap — наш KubeProxyConfiguration с mode: nftables — на Windows не используется: параметры задаются флагами. Толерантность operator: Exists пускает под на узел с любым taint.
Образ собирает SIG Windows: внутри официальный kube-proxy.exe и скрипт start.ps1, который ждёт появления HNS-сети Calico и передаёт её имя в переменной KUBE_NETWORK; для сети типа Overlay ждёт конечную точку Calico_ep и передаёт его адрес флагом --source-vip; удаляет старые политики балансировки в HNS (kube-proxy не умеет согласовывать с ними состояние) и запускает kube-proxy с --proxy-mode=kernelspace и --enable-dsr=true. Что такое режим kernelspace, source VIP и DSR, разобрано в прошлой части.
$ kubectl -n kube-system get pods -o wide --field-selector spec.nodeName=k8s-lab-win-1
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-proxy-windows-z9qdq 1/1 Running 0 30s 192.168.10.25 k8s-lab-win-1 <none> <none>
$ kubectl -n kube-system logs -l k8s-app=kube-proxy-windows | head -6
WARNING: The names of some imported commands from the module 'hns' include unapproved verbs that might make them less
discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose
parameter. For a list of approved verbs, type Get-Verb.
Running kub-proxy service.
Waiting for HNS network Calico to be created...
HNS network Calico found.
Предупреждение о «неодобренных глаголах» — от вспомогательного модуля hns со старыми именами команд, опечатка kub-proxy — из самого скрипта; дальше видно, как он ждёт сеть Calico и запускается.
Taint, RuntimeClass и Linux-DaemonSet'ы
Kubernetes сам ставит на узлы метки kubernetes.io/os и kubernetes.io/arch, а на Windows-узел — ещё node.kubernetes.io/windows-build:
$ kubectl get node k8s-lab-win-1 -L kubernetes.io/os,node.kubernetes.io/windows-build
NAME STATUS ROLES AGE VERSION OS WINDOWS-BUILD
k8s-lab-win-1 Ready <none> 6m6s v1.37.2 windows 10.0.26100
Поды по узлам разводят три механизма из части 13, каждый со своей задачей.
Taint os=windows:NoSchedule защищает Windows-узел от всего, что про Windows не знает: Deployment без nodeSelector или чарт, написанный в расчёте на Linux, на него не попадут — у их подов нет толерантности.
RuntimeClass избавляет Windows-поды от необходимости повторять nodeSelector и толерантность в каждом манифесте. Это объект кластерного уровня, связывающий имя с обработчиком containerd и условиями размещения:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: windows-2025
handler: runhcs-wcow-process
scheduling:
nodeSelector:
kubernetes.io/os: windows
kubernetes.io/arch: amd64
node.kubernetes.io/windows-build: "10.0.26100"
tolerations:
- key: os
operator: Equal
value: windows
effect: NoSchedule
$ kubectl apply -f runtimeclass-windows-2025.yaml
runtimeclass.node.k8s.io/windows-2025 created
Admission-плагин RuntimeClass дописывает в под с runtimeClassName: windows-2025 селектор и толерантность из scheduling, а kubelet передаёт containerd имя обработчика. Сборка в селекторе делает имя класса честным: образ ltsc2025 на Windows Server 2022 не запустится, и у узлов каждой версии будет свой RuntimeClass. Поле spec.os.name: windows RuntimeClass не ставит, его пишут явно: по нему API-сервер отклоняет Linux-поля securityContext, а Pod Security Admission смягчает неприменимые к Windows проверки.
nodeSelector: kubernetes.io/os: linux у Linux-DaemonSet'ов — третья часть, о которой легко забыть. Системные DaemonSet'ы часто толерантны ко всему (operator: Exists), чтобы работать на любых узлах; taint их не остановит, на Linux их держит только селектор. Проверим, что стоит в тестовом кластере:
$ kubectl get ds -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODE-SELECTOR:.spec.template.spec.nodeSelector,TOLERATIONS:.spec.template.spec.tolerations[*].operator'
NAMESPACE NAME NODE-SELECTOR TOLERATIONS
calico-system calico-node map[kubernetes.io/os:linux] Exists,Exists,Exists
calico-system calico-node-windows map[kubernetes.io/os:windows] Exists,Exists,Exists
calico-system csi-node-driver map[kubernetes.io/os:linux] Exists,Exists,Exists
kube-system kube-proxy map[kubernetes.io/os:linux] Exists
kube-system kube-proxy-windows map[kubernetes.io/os:windows] Exists,Exists
Селектор у kube-proxy записан в манифесте kubeadm, у calico-node и csi-node-driver его ставит оператор Tigera — каждому компоненту по ОС, которую тот поддерживает. Тестовый кластер молодой, и других DaemonSet'ов в нём пока нет; они появятся в части 39 вместе с мониторингом и хранилищем. Хороший пример того, почему правило важно, — node-exporter из kube-prometheus-stack (часть 32). Его чарт по умолчанию толерантен к любому taint с эффектом NoSchedule — чтобы собирать метрики и с мастеров, — а значит, и к os=windows:NoSchedule:
$ helm show values oci://ghcr.io/prometheus-community/charts/prometheus-node-exporter | grep -A3 -E '^(nodeSelector|tolerations):'
nodeSelector:
kubernetes.io/os: linux
# kubernetes.io/arch: amd64
tolerations:
- effect: NoSchedule
operator: Exists
На Linux его удерживает только nodeSelector: kubernetes.io/os: linux, который чарт, к счастью, ставит сам. Кто переопределит nodeSelector в своих values, например чтобы добавить метку архитектуры, и забудет про ОС, получит под node-exporter с Linux-образом на Windows-узле — вечный ErrImagePull.
Правило: у любого DaemonSet'а с Linux-образом должен быть селектор kubernetes.io/os: linux, даже если сейчас его спасает taint; у каждого нового чарта это проверяют в значениях по умолчанию (helm show values) и после установки той же командой kubectl get ds -A. Агенты логов и метрик для самих Windows-узлов — в части 39.
Проверка: Windows-под, Service, DNS и связность
Повторим проверку из части 4 поперёк ОС: под на Linux-узле обращается к Windows-поду и наоборот — по адресам подов, через Service и по именам DNS. Windows-приложение возьмём готовое — пример веб-приложения на .NET, который Microsoft публикует в образе на основе Nano Server, mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025. Тег плавающий и указывает на самую новую версию .NET — на момент написания это .NET 11 RC; для рабочих приложений образ собирают сами, об этом часть 39. Образ весит около 260 МБ — против 2,4 ГБ у IIS на Server Core, — но скачаем его на узел заранее:
PS C:\> crictl pull mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025
Image is up to date for sha256:35169c38821e233b05473c07337b96034a71bbab3cda564a2817d5e9bdb0fd00
PS C:\> crictl images | Select-String samples
mcr.microsoft.com/dotnet/samples aspnetapp-nanoserver-ltsc2025 35169c38821e2 258MB
Образы на Nano Server работают от непривилегированной учётной записи ContainerUser, поэтому неймспейсу win-demo можно дать тот же уровень restricted Pod Security Admission, что и demo, — как разобрано в части 36, для Windows-пода он требует только runAsNonRoot:
apiVersion: v1
kind: Namespace
metadata:
name: win-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.37
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: aspnetapp
namespace: win-demo
labels:
app.kubernetes.io/name: aspnetapp
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: aspnetapp
template:
metadata:
labels:
app.kubernetes.io/name: aspnetapp
spec:
os:
name: windows
runtimeClassName: windows-2025
securityContext:
runAsNonRoot: true
windowsOptions:
runAsUserName: ContainerUser
containers:
- name: aspnetapp
image: mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /
port: http
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: aspnetapp
namespace: win-demo
spec:
selector:
app.kubernetes.io/name: aspnetapp
ports:
- name: http
port: 80
targetPort: http
$ kubectl apply -f aspnetapp.yaml
namespace/win-demo created
deployment.apps/aspnetapp created
service/aspnetapp created
$ kubectl -n win-demo get pods,svc -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/aspnetapp-769559d8ff-dkp9z 1/1 Running 0 56s 172.21.41.6 k8s-lab-win-1 <none> <none>
pod/aspnetapp-769559d8ff-vk445 1/1 Running 0 57s 172.21.41.5 k8s-lab-win-1 <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/aspnetapp ClusterIP 10.102.98.171 <none> 80/TCP 57s app.kubernetes.io/name=aspnetapp
$ kubectl -n win-demo get pod aspnetapp-769559d8ff-dkp9z -o json | jq '{nodeSelector: .spec.nodeSelector, tolerations: [.spec.tolerations[] | select(.key == "os")]}'
{
"nodeSelector": {
"kubernetes.io/arch": "amd64",
"kubernetes.io/os": "windows",
"node.kubernetes.io/windows-build": "10.0.26100"
},
"tolerations": [
{
"effect": "NoSchedule",
"key": "os",
"operator": "Equal",
"value": "windows"
}
]
}
$ kubectl -n win-demo exec aspnetapp-769559d8ff-dkp9z -- cmd /c "echo %USERNAME% & ver"
ContainerUser
Microsoft Windows [Version 10.0.26100.33674]
Под прошёл restricted без единого предупреждения. В манифесте не было ни nodeSelector, ни толерантности os=windows — их дописал admission-плагин RuntimeClass; остальные толерантности пода (not-ready, unreachable) Kubernetes добавляет любому поду, поэтому выбираем нужную фильтром. Внутри — ContainerUser, а ревизия Windows в образе (26100.33674) новее, чем на хосте (26100.33438): совпадать должна только сборка. whoami и PowerShell в Nano Server нет, поэтому — cmd /c. Служебные адреса блока 172.21.41.0/26 подам не достаются: .0 — адрес сети, .1 — шлюз HNS-сети, .2 — Calico_ep, .63 — последний адрес блока; поды получили .5 и .6, а .3 и .4 до них успели побывать у других подов — адреса не переиспользуются сразу.
С Linux на Windows
Под check создаём в неймспейсе default: на нём нет ограничений Pod Security, а ping в busybox требует прав root, которых restricted не даст. Без RuntimeClass taint не пустит под на Windows-узел — он окажется на одном из Linux-воркеров, у нас на k8s-lab-node-2:
$ kubectl run -i --rm check --image=busybox:1.37 --restart=Never -- sh -c '
nslookup aspnetapp.win-demo.svc.cluster.local | tail -3
wget -qO- http://aspnetapp.win-demo.svc.cluster.local | grep -o "<title>[^<]*</title>"
ping -c 2 172.21.41.6'
Name: aspnetapp.win-demo.svc.cluster.local
Address: 10.102.98.171
<title>Welcome to .NET - AspNetCoreRazorApp</title>
PING 172.21.41.6 (172.21.41.6): 56 data bytes
64 bytes from 172.21.41.6: seq=0 ttl=126 time=0.405 ms
64 bytes from 172.21.41.6: seq=1 ttl=126 time=0.489 ms
...
pod "check" deleted from default namespace
k8s-lab-node-2 (Linux) k8s-lab-win-1 (Windows)
┌────────────────────────────┐ ┌────────────────────────────┐
│ под check на этом узле │ │ под aspnetapp 172.21.41.6 │
│ │ aspnetapp.win-demo... │ │ ▲ │
│ │ CoreDNS → 10.102.98.171│ │ │ VFP: ACL от Felix │
│ ▼ │ │ │ │
│ nftables kube-proxy: DNAT │ │ vSwitch, сеть «Calico»: │
│ 10.102.98.171:80 → │ │ снять заголовок VXLAN │
│ 172.21.41.6:8080 │ │ ▲ │
│ ▼ │ UDP 4789 │ │ │
│ vxlan.calico, VNI 4096 ────┼──────────────►┼───┘ │
└────────────────────────────┘ .26 → .25 └────────────────────────────┘
Имя сервиса разрешил CoreDNS (часть 16), ClusterIP превратил в адрес Windows-пода kube-proxy на Linux-узле — правилами nftables, ему всё равно, на какой ОС работает под. Пакет к блоку 172.21.41.0/26 ушёл в vxlan.calico — маршрут к нему Felix прописал, как только Windows-узел получил блок, — на Windows-узле VFP снял заголовок VXLAN и доставил пакет на порт пода. TTL 126: Windows ставит 128, а на обратном пути ответ проходит две маршрутизации — шлюз HNS-сети на Windows-узле и ядро Linux-узла. С самого Linux-узла ping того же пода показал бы ttl=127.
С Windows на Linux
Теперь из Windows-пода к hello в неймспейсе demo, который мы развернули на Linux-воркерах. В Nano Server нет nslookup, зато есть curl.exe и ping; какой адрес получил клиент из DNS, покажет сам curl.exe:
$ kubectl -n win-demo exec deploy/aspnetapp -- curl.exe -s -o NUL -w "%{remote_ip} %{http_code}\n" http://hello.demo.svc.cluster.local/healthz
10.97.141.163 200
$ kubectl -n win-demo exec deploy/aspnetapp -- ping -n 2 172.21.172.4
Pinging 172.21.172.4 with 32 bytes of data:
Reply from 172.21.172.4: bytes=32 time=13ms TTL=63
Reply from 172.21.172.4: bytes=32 time<1ms TTL=63
...
Здесь ClusterIP сервиса hello в адрес Linux-пода превратил уже kube-proxy для Windows — политикой балансировки HNS на порту пода. Особенности Windows-подов, о которых шла речь в прошлой части, — в действии:
$ kubectl -n win-demo exec deploy/aspnetapp -- curl.exe -s -o NUL -w "%{remote_ip} %{http_code}\n" --max-time 5 http://hello.demo/healthz
000
command terminated with exit code 6
$ kubectl -n win-demo exec deploy/aspnetapp -- curl.exe -s -o NUL -w "%{remote_ip} %{http_code}\n" http://aspnetapp/
10.102.98.171 200
$ kubectl -n win-demo exec deploy/aspnetapp -- ping -n 2 1.1.1.1 | tail -3
Packets: Sent = 2, Received = 2, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 9ms, Maximum = 9ms, Average = 9ms
- Имя с точкой не дополняется суффиксами поиска. Код выхода 6 у
curl.exe— «не удалось разрешить имя»:hello.demoклиент DNS в Windows не дополняет, аaspnetappбез точки — дополняет. К сервисам других неймспейсов из Windows-подов обращаются по полному имени. pingнаружу у нас работает. Документация Kubernetes перечисляет исходящий ICMP через NAT среди ограничений, но на Windows Server 2025 с Calico в режиме VXLAN ответ вернулся. Проверять связность наружу всё равно надёжнее по TCP —curl.exe.
И ограничение самого узла: Windows-хост к адресам сервисов не ходит — curl.exe http://10.102.98.171 в PowerShell на k8s-lab-win-1 ответа не дождётся, а по адресу пода дождётся.
Смотрим руками
Теперь, когда кластер собран, посмотрим вживую на то, что в частях 36 и 37 разбиралось на схемах.
Кластер целиком
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-lab-master-1 Ready control-plane 153m v1.37.2 192.168.10.23 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
k8s-lab-node-1 Ready <none> 146m v1.37.2 192.168.10.24 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
k8s-lab-node-2 Ready <none> 146m v1.37.2 192.168.10.26 <none> Ubuntu 26.04.1 LTS 7.0.0-42-generic (amd64) containerd://2.2.2
k8s-lab-win-1 Ready <none> 64m v1.37.2 192.168.10.25 <none> Windows Server 2025 Datacenter 10.0.26100.33438 (amd64) containerd://2.2.2
$ kubectl top nodes
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
k8s-lab-master-1 121m 6% 1588Mi 48%
k8s-lab-node-1 34m 1% 1960Mi 26%
k8s-lab-node-2 55m 2% 1514Mi 20%
k8s-lab-win-1 8m 0% 1292Mi 16%
Колонки OS-IMAGE и KERNEL-VERSION у Windows-узла — название редакции и полная версия ОС с ревизией. Имя узла — короткое k8s-lab-win-1, как у Linux-узлов: kubelet регистрирует узел по имени компьютера. kubectl top видит и Windows-узел: kubelet на Windows отдаёт /metrics/resource так же, как на Linux, а metrics-server проверяет его серверный сертификат, выпущенный по одобренному CSR.
Службы и поды на узле
PS C:\> Get-Service kubelet, containerd, vmcompute, hns | Format-Table -AutoSize Status, Name, StartType
Status Name StartType
------ ---- ---------
Running containerd Automatic
Running hns Manual
Running kubelet Automatic
Running vmcompute Manual
PS C:\> crictl pods
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
df1924ad959c6 4 minutes ago Ready aspnetapp-769559d8ff-dkp9z win-demo 0 runhcs-wcow-process
690ccfd48f5c4 4 minutes ago Ready aspnetapp-769559d8ff-vk445 win-demo 0 runhcs-wcow-process
7fdf200e6eea1 59 minutes ago Ready kube-proxy-windows-z9qdq kube-system 0 (default)
b1da3ea54ae81 About an hour ago Ready calico-node-windows-q54h2 calico-system 0 (default)
vmcompute (HCS) и hns запускаются по требованию, когда к ним обращается containerd или CNI-плагин. В колонке RUNTIME у подов aspnetapp — обработчик runhcs-wcow-process, который kubelet взял из RuntimeClass, а (default) у HostProcess-подов — обработчик по умолчанию, в нашей конфигурации тот же runhcs-wcow-process.
Процессы контейнера на хосте
При изоляции процессов процессы контейнера — процессы хоста. У каждого контейнера свой сеанс, и по номеру сеанса (SI) их легко отделить. Найдём сеанс приложения и посмотрим, что ещё в нём работает:
PS C:\> Get-Process -Name AspNetCoreRazorApp | Format-Table -AutoSize Id, SI, ProcessName
Id SI ProcessName
-- -- -----------
144 5 AspNetCoreRazorApp
8228 4 AspNetCoreRazorApp
PS C:\> Get-Process | Where-Object SessionId -eq 5 | Sort-Object ProcessName | Format-Table -AutoSize Id, SI, ProcessName
Id SI ProcessName
-- -- -----------
144 5 AspNetCoreRazorApp
8872 5 CExecSvc
8736 5 csrss
912 5 lsass
6168 5 services
192 5 svchost
...
3528 5 wininit
Два процесса приложения — два пода, каждый в своём сеансе. В сеансе контейнера — собственные csrss, wininit, services, lsass и десяток svchost, служба исполнения CExecSvc и само приложение — та самая картина со схемы из части 36. Диспетчера сеансов smss в этой выборке нет, хотя у контейнера он свой и живёт всё время: он работает в сеансе 0, а мы отбирали процессы по сеансу контейнера. А HostProcess-контейнеры — обычные процессы хоста в сеансе 0, рядом с системными службами самого узла, и запущены они из каталога, куда HCS подключил содержимое образа:
PS C:\> Get-Process calico, kube-proxy | Format-Table -AutoSize Id, SI, ProcessName, Path
Id SI ProcessName Path
-- -- ----------- ----
852 0 calico C:\hpc\CalicoWindows\calico.exe
3856 0 calico C:\hpc\CalicoWindows\calico.exe
4308 0 kube-proxy C:\hpc\kube-proxy\kube-proxy.exe
Два процесса calico — два контейнера пода calico-node-windows, felix и node: Calico для Windows собран в один исполняемый файл с подкомандами. C:\hpc — значение CONTAINER_SANDBOX_MOUNT_POINT для этих подов.
Сеть пода в HNS
Сетевые адаптеры узла:
PS C:\> Get-NetAdapter | Format-Table Name, InterfaceDescription, MacAddress
Name InterfaceDescription MacAddress
---- -------------------- ----------
Ethernet Microsoft Hyper-V Network Adapter 00-15-5D-0A-00-05
vEthernet (Ethernet) Hyper-V Virtual Ethernet Adapter 00-15-5D-0A-00-05
vEthernet (DrPort 1803C339-EAB3-4933-8F1F-62E7D36C6E21) Hyper-V Virtual Ethernet Container Adapter 00-15-5D-EB-90-AE
vEthernet (df1924ad959c693c6dc07f1f797f63458fa53dcc81f8126755c3a4509e87a15e_Calico) Hyper-V Virtual Ethernet Container Adapter #3 0E-2A-AC-15-29-06
...
vEthernet (DrPort …) — порт распределённого маршрутизатора (distributed router) в vSwitch: это шлюз 172.21.41.1 сети Calico. Адаптеры подов названы по идентификатору песочницы пода, и последние четыре байта их MAC-адреса — адрес пода в шестнадцатеричном виде: AC-15-29-06 — это 172.21.41.6.
HNS-сеть Calico и её маршруты к блокам других узлов:
PS C:\> $net = Get-HnsNetwork | Where-Object Name -eq Calico
PS C:\> $net.Subnets.Policies | ConvertTo-Json -Compress
{"Type":"VSID","VSID":4096}
PS C:\> $net.Policies | Where-Object Type -eq RemoteSubnetRoute |
Format-Table DestinationPrefix, ProviderAddress, DistributedRouterMacAddress, IsolationId
DestinationPrefix ProviderAddress DistributedRouterMacAddress IsolationId
----------------- --------------- --------------------------- -----------
172.21.211.0/26 192.168.10.24 66-ee-f4-61-98-50 4096
172.21.70.192/26 192.168.10.23 66-79-81-e0-1a-7d 4096
172.21.172.0/26 192.168.10.26 66-05-38-46-ab-ea 4096
Это политики RemoteSubnetRoute из прошлой части: блок соседа, адрес его узла и MAC его VTEP — интерфейса vxlan.calico на Linux-узле. Сверим с Linux-стороной:
user@k8s-lab-node-1:~$ ip -d link show vxlan.calico | head -2
5: vxlan.calico: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether 66:ee:f4:61:98:50 brd ff:ff:ff:ff:ff:ff promiscuity 0 allmulti 0 minmtu 68 maxmtu 65535
user@k8s-lab-node-1:~$ ip route | grep 172.21.41
172.21.41.0/26 via 172.21.41.1 dev vxlan.calico onlink mtu 1450
user@k8s-lab-node-1:~$ ip neigh show dev vxlan.calico
172.21.70.192 lladdr 66:79:81:e0:1a:7d PERMANENT
172.21.172.0 lladdr 66:05:38:46:ab:ea PERMANENT
172.21.41.1 lladdr 00:15:5d:eb:90:ae PERMANENT
user@k8s-lab-node-1:~$ bridge fdb show dev vxlan.calico
66:05:38:46:ab:ea dst 192.168.10.26 self permanent
66:79:81:e0:1a:7d dst 192.168.10.23 self permanent
00:15:5d:eb:90:ae dst 192.168.10.25 self permanent
MAC 66:ee:f4:61:98:50 из ip -d link — тот, что записан на Windows-узле для блока k8s-lab-node-1. В обратную сторону: блок Windows-узла 172.21.41.0/26 идёт через vxlan.calico на шлюз 172.21.41.1, ARP-запись ведёт его к MAC порта DrPort, а FDB — к адресу Windows-узла. Откуда Felix на Linux берёт эти значения, видно в аннотациях объекта Node:
$ kubectl get node k8s-lab-win-1 -o json | jq '.metadata.annotations | with_entries(select(.key | test("VXLAN")))'
{
"projectcalico.org/IPv4VXLANTunnelAddr": "172.21.41.1",
"projectcalico.org/VXLANTunnelMACAddr": "00:15:5d:eb:90:ae"
}
У Linux-узлов туннельный адрес — первый адрес блока (172.21.70.192, 172.21.211.0, 172.21.172.0), а у Windows-узла — шлюз HNS-сети .1, и MAC его VTEP Calico записывает явно: на Windows это не интерфейс ядра, а порт vSwitch.
Конечные точки HNS — локальные (поды) и удалённые, которые kube-proxy создал для бэкендов и адресов сервисов:
PS C:\> Get-HnsEndpoint | Sort-Object IsRemoteEndpoint, IPAddress | Format-Table IPAddress, MacAddress, IsRemoteEndpoint
IPAddress MacAddress IsRemoteEndpoint
--------- ---------- ----------------
10.102.98.171 00:15:5d:0a:00:05 True
10.96.0.1 00:15:5d:0a:00:05 True
10.96.0.10 00:15:5d:0a:00:05 True
10.97.141.163 00:15:5d:0a:00:05 True
...
172.21.172.3 02-11-ac-15-ac-03 True
172.21.172.4 02-11-ac-15-ac-04 True
172.21.211.25 02-11-ac-15-d3-19 True
172.21.41.2 00:15:5d:0a:00:05 True
172.21.70.199 02-11-ac-15-46-c7 True
...
172.21.41.5 0E-2A-ac-15-29-05
172.21.41.6 0E-2A-ac-15-29-06
172.21.211.25 и 172.21.172.4 — поды hello, 172.21.172.3 и 172.21.70.199 — CoreDNS; удалённые конечные точки есть и для самих ClusterIP, и для Calico_ep. MAC у удалённых конечных точек подов не настоящий: kube-proxy составляет его из префикса 02-11 и четырёх байт адреса (ac-15-d3-19 — 172.21.211.25), а у ClusterIP и Calico_ep записан MAC самого узла. Доставляет к ним не порт коммутатора, а политики маршрутизации сети. Политики конечной точки пода:
PS C:\> $ep = Get-HnsEndpoint | Where-Object IPAddress -eq 172.21.41.6
PS C:\> $ep.Policies | Group-Object Type | Format-Table Count, Name
Count Name
----- ----
2 OutBoundNAT
1 ROUTE
1 PA
7 ACL
PS C:\> ($ep.Policies | Where-Object { $_.Type -eq 'OutBoundNAT' -and $_.ExceptionList }).ExceptionList
10.96.0.0/12
172.21.0.0/16
OutBoundNAT с исключениями — это natOutgoing: всё, что идёт не в сеть подов и не в сеть сервисов, получает адрес узла; сеть сервисов в списке — та самая serviceCIDRs из Installation. PA (provider address) и ROUTE связывают конечную точку с адресом узла в VXLAN-сети, ACL-записи — политики, которые Felix разложил на эту конечную точку; без NetworkPolicy в неймспейсе там правила «разрешить» и служебные правила для узла.
Балансировщики kube-proxy показывает hnsdiag — утилита диагностики HNS из состава Windows Server:
PS C:\> hnsdiag list loadbalancers
Load Balancer : 996c2141-38f4-4d34-b971-781a68c61678
Virtual IPs : 10.96.0.10
Direct IP IDs : 2b87a046-19b9-458c-a681-5ff88360106d
0f8dc4db-f502-48d3-8845-29772ed1ea03
...
Load Balancer : 6d740400-5fdb-4792-9d80-f05533d763e1
Virtual IPs : 10.96.0.10
Direct IP IDs : 2b87a046-19b9-458c-a681-5ff88360106d
0f8dc4db-f502-48d3-8845-29772ed1ea03
...
Каждый порт каждого сервиса — отдельный балансировщик со списком конечных точек-бэкендов: то, что на Linux-узле лежит в таблице ip kube-proxy nftables. У DNS (10.96.0.10) их два — на UDP и TCP, оба ссылаются на одни и те же конечные точки подов CoreDNS.
Linux-поля в Windows-поде
И последнее из части 36 — проверка os.name в API. Под, в который по привычке скопирован securityContext с Linux-узла:
$ kubectl -n win-demo apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: bad
spec:
os:
name: windows
runtimeClassName: windows-2025
securityContext:
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: shell
image: mcr.microsoft.com/windows/nanoserver:ltsc2025
EOF
The Pod "bad" is invalid:
* spec.securityContext.seccompProfile: Forbidden: cannot be set for a windows pod
* spec.securityContext.runAsUser: Forbidden: cannot be set for a windows pod
API отказал до записи в etcd. Без os.name тот же под был бы принят — Pod Security проверила бы его как Linux-под и выдала бы предупреждения про allowPrivilegeEscalation, capabilities и runAsNonRoot, — а Linux-поля были бы молча проигнорированы на узле. Поэтому os.name стоит указывать всегда, и для Linux-подов тоже.
Если узел NotReady
Начинают, как и на Linux, с условий узла: kubectl describe node k8s-lab-win-1, раздел Conditions.
NetworkPluginNotReady ... cni plugin not initialized — в C:\etc\cni\net.d нет конфигурации, значит, не отработал calico-node-windows:
- пода нет совсем — проверить
windowsDataplane: HNSв Installation иkubectl get tigerastatus: оператор пишет туда причину, как в случае соstrictAffinityвыше; - под висит в
InitилиContainerCreating—kubectl -n calico-system describe pod <под>: чаще всего это долгая загрузка образов или ошибка загрузки; - под перезапускается — логи по контейнерам:
-c install-cni, затем-c nodeи-c felix. Ошибкаdial tcp 10.96.0.1:443: i/o timeoutозначает, что нет ConfigMapkubernetes-services-endpoint. Провалы проб Felix в первую минуту после старта — нормальны.
Kubelet stopped posting node status или статус Unknown — kubelet не достучался до API. На узле — Get-Service kubelet, containerd, Get-Content C:\var\log\kubelet\kubelet.err.log -Tail 30 и Test-NetConnection k8s-lab-api.example.com -Port 6443. Частые причины:
- соединение потерялось при создании vSwitch — адрес хоста переехал на
vEthernet (Ethernet), и kubelet застрял на старом соединении; документация Calico прямо советует после установкиRestart-Service kubelet; - часы —
x509: certificate has expired or is not yet validв логе, смотретьw32tm /query /status; - containerd не работает —
kubeletот него зависит и без него не стартует;crictl infoпокажет, отвечает ли containerd, а журнал System с источникомService Control Manager(Get-WinEvent) — почему служба упала.
Узел Ready, но поды не работают — разбирают тут же:
The container operating system does not match the host operating systemв событиях пода — сборка образа не совпадает со сборкой узла; лечится правильным тегом образа или RuntimeClass;kube-proxy-windowsвCrashLoopBackOffс ожиданием сетиCalicoв логе — сначала чинить Calico;kubectl logsиexecпадают сtls: internal error— не одобрен CSRkubelet-serving; с тайм-аутом — закрыт порт 10250;- Windows-поды не видят поды Linux-узлов, а внутри узла всё работает — закрыт UDP 4789 на одной из сторон: в файрволе Linux-узла или в файрволе гипервизора между виртуальными машинами.
Пока на узле нет ценного, поставить его заново дешевле, чем чинить:
$ kubectl drain k8s-lab-win-1 --ignore-daemonsets --delete-emptydir-data
$ kubectl delete node k8s-lab-win-1
PS C:\> kubeadm reset -f
PS C:\> Get-HnsNetwork | Remove-HnsNetwork
PS C:\> Remove-Item C:\etc\cni\net.d\* -Force
PS C:\> Restart-Computer
kubeadm reset на Windows, как и на Linux, не трогает чужое: HNS-сеть и конфигурацию CNI удаляют руками, а перезагрузка убирает виртуальный коммутатор и возвращает адрес на физический адаптер. Если же неудачной оказалась Linux-основа, а в тестовом кластере ещё ничего нет, проще всего пройти раздел «Если что-то пошло не так» части 4 на всех трёх Linux-узлах и начать с kubeadm init заново — именно для этого кластер и тестовый. Удаляя /etc/cni/net.d/* на Linux-узле, учтите, что каталог закрыт для обычного пользователя: шаблон * в sudo rm /etc/cni/net.d/* раскрывает ещё не root, и файлы остаются — нужен sudo sh -c 'rm -f /etc/cni/net.d/*'.
Источники
- Kubernetes: Adding Windows worker nodes, kubeadm Configuration (v1beta4), Windows containers in Kubernetes, Guide for running Windows containers, Networking on Windows, Create a Windows HostProcess Pod, Pod Security Standards, kubectl config set, Organizing cluster access using kubeconfig files.
- sig-windows-tools:
Install-Containerd.ps1,PrepareNode.ps1,calico/kube-proxy. - Calico: установка на свой кластер через оператор, нативные CRD v3, справочник Installation.
- Calico для Windows: установка через оператор, требования, ограничения, диагностика.
- metrics-server, установка cert-manager, значения чарта prometheus-node-exporter.
- Microsoft: Windows container networking, Windows container version compatibility, Windows Time service tools and settings, образы примеров .NET.
Дальше — рабочие нагрузки, хранилище и наблюдаемость на Windows: образы servercore и nanoserver и мультиплатформенные манифесты с os.version, сборка hello под windows/amd64, чем requests и limits на Windows отличаются от Linux, пробы и корректное завершение, CSI на Windows-узлах через csi-proxy и HostProcess-плагины, csi-driver-smb в тестовом кластере и на Windows-узле и почему NFS остаётся за Linux-узлами, kube-prometheus-stack с windows-exporter в тестовом кластере, Loki в тестовом кластере и логи Windows-узла в нём, ежемесячное обновление узлов.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.