Абстракт
Когда я был маленьким и глупым, в своей домашней лаборатории я развернул kuber‑barick — его возможностей хватало на баловство с нейронками и обучением крутить поды, но при попытке поднять второй (например, для стейджинга) всё упёрлось в дефицит железа и ручную пляску с самоподписными сертификатами. Решил перейти на более автоматный подход: наткнулся на Talos и Cluster API (CAPI). В статье покажу, как их связка в OpenStack позволяет быстро, повторяемо и без лишних ручных движений разворачивать мультикластер.

Что вообще за существа такие Talos и CAPI
Talos — это минималистичная и полностью упрощённая immutable операционная система, созданная специально для развёртывания и управления Kubernetes-кластером. Про неё тут уже были статьи
Cluster API — это официальный проект CNCF, предоставляющий декларативный подход к управлению жизненным циклом Kubernetes-кластеров независимо от инфраструктуры. Про этого зверя статья была лишь одна. Возмутительно, и пора исправить!
Поехали!
Из Openstack понадобится такой список
Keystone
Glance
Placement
Cinder
Nova
Neutron
Octavia
В случае если видеокарты не нужны можно идти к установке. А если они для проекта нужны то заострю внимание лишь на Nova. Пришлось добавить туда PCI passthrough.
В /etc/default/grub
Надо подкинуть vfio-pci к всем pci-e девайсом видеокарт. Vendor и product id от карточки к карточке разный, даже у одной и той же модели, поэтому надо посмотреть вывод lspci -nn | grep NVIDIA
GRUB_CMDLINE_LINUX="... vfio-pci.ids=10de:1e02,10de:10f7,10de:1ad6,10de:1ad7 rd.driver.blacklist=nouveau,nvidiafb,snd_hda_intel,xhci_hcd,i2c_nvidia_gpu"
В /etc/nova/nova.conf
на воркеры с видеокартами надо докинуть алиас
[pci]
passthrough_whitelist = {\"vendor_id\":\"10de\",\"product_id\":\"1e02\"}
passthrough_whitelist = {\"vendor_id\":\"10de\",\"product_id\":\"10f7\"}
passthrough_whitelist = {\"vendor_id\":\"10de\",\"product_id\":\"1ad6\"}
passthrough_whitelist = {\"vendor_id\":\"10de\",\"product_id\":\"1ad7\"}
alias = {\"vendor_id\":\"10de\",\"product_id\":\"1e02\",\"name\":\"gpu\"}
alias = {\"vendor_id\":\"10de\",\"product_id\":\"10f7\",\"name\":\"gpu_audio\"}
alias = {\"vendor_id\":\"10de\",\"product_id\":\"1ad6\",\"name\":\"usb_controller\"}
alias = {\"vendor_id\":\"10de\",\"product_id\":\"1ad7\",\"name\":\"ucsi_controller\"}
Создаем типы инстансов для GPU
#!/bin/bash
. .secrets
flavors=(
# ========== GPU Optimized (G-серия) ==========
"401 8 16384 40 g1.tiny"
"402 16 32768 40 g1.small"
"403 24 65536 40 g1.medium"
"404 32 131072 40 g1.large"
"405 32 196608 40 g1.xlarge"
)
for flavor in "${flavors[@]}"; do
openstack flavor create --id "$id" --vcpus "$vcpus" --ram "$ram" --disk "$disk" "$name" \
--property "pci_passthrough:alias"="gpu:1,gpu_audio:1,usb_controller:1,ucsi_controller:1" > /dev/null
done
Установка: подготовка вспомогательного k8s
Создадим Kubernetes для создания Kubernetes-а. Для примера Kubernetes in docker на микровиртуалке ubuntu в openstack. Как депенденси KinD нужен docker. Поэтому в скрипт я вложил инструкцию отсюда.
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.17.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
sudo reboot #чтобы сокет докера встал с нужными правами
Стартуем KinD:
kind create cluster --name management
Устанавливаем клиент clusterctl пользуясь руководством
curl -L https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.9.6/clusterctl-linux-amd64 -o clusterctl
chmod +x ./clusterctl
sudo mv ./clusterctl /usr/local/bin/clusterctl
Инициализируем clusterctl и необходимый для установки openstack-resource-controller
clusterctl init --infrastructure openstack --bootstrap talos --control-plane talos --core cluster-api
kubectl apply -f https://github.com/k-orc/openstack-resource-controller/releases/download/v1.0.1/install.yaml
Создаем секрет с clouds.yaml
clouds:
openstack:
auth:
auth_url: http://controller:5000/v3/
username: "username"
password: "pa$$word"
project_id: "project_id"
project_name: "project_name"
user_domain_name: "Default"
region_name: "nova"
interface: "public"
identity_api_version: 3
Добавляем его в темповый кубер
kubectl create secret generic cloud-config --namespace=kube-system --from-file=clouds.yaml="$HOME/.config/openstack/clouds.yaml" --from-literal=cacert="" --dry-run=client -o yaml | kubectl apply -f -
Установка: YAML‑программируем таргетный k8s
Создадим базовый набор CRD‑шек
cluster.yaml
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: k8s-cluster
namespace: kube-system
spec:
clusterNetwork:
pods:
cidrBlocks:
- 192.168.0.0/16
serviceDomain: cluster.local
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha3
kind: TalosControlPlane
name: k8s-cluster-control-plane
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackCluster
name: k8s-cluster
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackCluster
metadata:
name: k8s-cluster
namespace: kube-system
spec:
apiServerLoadBalancer:
enabled: true
externalNetwork:
id: # НАДО УКАЗАТЬ ID ВНЕШНЕЙ СЕТИ OPENSTACK #
identityRef:
cloudName: openstack
name: k8s-cluster-cloud-config
managedSecurityGroups:
allNodesSecurityGroupRules:
- name: allow-port-talos
direction: ingress
etherType: IPv4
portRangeMin: 50000
portRangeMax: 50001
protocol: tcp
remoteIPPrefix: 0.0.0.0/0
- name: allow-pod-traffic # Далее правила для Flannel #
direction: ingress
etherType: IPv4
portRangeMin: 1
portRangeMax: 65535
protocol: tcp
remoteIPPrefix: 192.168.0.0/16
- name: allow-pod-traffic-udp
direction: ingress
etherType: IPv4
portRangeMin: 1
portRangeMax: 65535
protocol: udp
remoteIPPrefix: 192.168.0.0/16
- name: allow-all-traffic-tcp
direction: ingress
etherType: IPv4
portRangeMin: 1
portRangeMax: 65535
protocol: tcp
remoteIPPrefix: 10.10.20.0/24
- name: allow-all-traffic-udp
direction: ingress
etherType: IPv4
portRangeMin: 1
portRangeMax: 65535
protocol: udp
remoteIPPrefix: 10.10.20.0/24
managedSubnets:
- cidr: 10.10.20.0/24
dnsNameservers:
- # НАДО УКАЗАТЬ DNS С КОТОРЫМ РАБОТАЕТ OPENSTACK #
Описываем воркеры
workers.yaml
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: k8s-cluster-md-0
namespace: kube-system
spec:
clusterName: k8s-cluster
replicas: 2
selector:
matchLabels:
machine-deployment: k8s-cluster-md-0
template:
metadata:
labels:
cluster.x-k8s.io/cluster-name: k8s-cluster
machine-deployment: k8s-cluster-md-0
spec:
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3
kind: TalosConfigTemplate
name: k8s-cluster-md-0
clusterName: k8s-cluster
failureDomain: nova # МОЖЕТ ОТЛИЧАТЬСЯ #
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
name: k8s-cluster-md-0
version: v1.30.0
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
metadata:
name: k8s-cluster-md-0
namespace: kube-system
spec:
template:
spec:
flavor: m1.large
image:
filter:
name: talos
sshKeyName: pgp-card
rootVolume:
sizeGiB: 50
type: __SSD__
---
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3
kind: TalosConfigTemplate
metadata:
name: k8s-cluster-md-0
namespace: kube-system
labels:
cluster.x-k8s.io/cluster-name: k8s-cluster
spec:
template:
spec:
generateType: worker
talosVersion: v1.9.5
strategicPatches:
- |
- op: replace
path: /machine/install
value:
disk: /dev/vda
- op: add
path: /machine/kubelet/extraArgs
value:
cloud-provider: external
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: k8s-cluster-md-gpu-0
namespace: kube-system
spec:
clusterName: k8s-cluster
replicas: 2
selector:
matchLabels:
machine-deployment: k8s-cluster-md-gpu-0
template:
metadata:
labels:
cluster.x-k8s.io/cluster-name: k8s-cluster
machine-deployment: k8s-cluster-md-gpu-0
spec:
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3
kind: TalosConfigTemplate
name: k8s-cluster-md-gpu-0
clusterName: k8s-cluster
failureDomain: nova # МОЖЕТ ОТЛИЧАТЬСЯ #
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
name: k8s-cluster-md-gpu-0
version: v1.30.0
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
metadata:
name: k8s-cluster-md-gpu-0
namespace: kube-system
spec:
template:
spec:
flavor: g1.small # МОЖЕТ ОТЛИЧАТЬСЯ #
image:
filter:
name: talos # МОЖЕТ ОТЛИЧАТЬСЯ #
sshKeyName: pgp-card # МОЖЕТ ОТЛИЧАТЬСЯ #
rootVolume:
sizeGiB: 50
type: __SSD__ # МОЖЕТ ОТЛИЧАТЬСЯ #
---
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3
kind: TalosConfigTemplate
metadata:
name: k8s-cluster-md-gpu-0
namespace: kube-system
labels:
cluster.x-k8s.io/cluster-name: k8s-cluster
spec:
template:
spec:
generateType: worker
talosVersion: v1.9.5
strategicPatches:
- |
- op: replace
path: /machine/install
value:
disk: /dev/vda # МОЖЕТ ОТЛИЧАТЬСЯ ОБРАЗ. СГЕНЕРИРУЙТЕ СВОЙ В TALOS FABRIC#
image: factory.talos.dev/installer/26124abcbd408be693df9fe852c80ef1e6cc178e34d7d7d8430a28d1130b4227:v1.9.5
- op: add
path: /machine/kernel
value: {}
- op: add
path: /machine/kubelet/extraArgs
value:
cloud-provider: external
- op: add
path: /machine/kernel/modules
value:
- name: nvidia
- name: nvidia_uvm
- name: nvidia_drm
- name: nvidia_modeset
- op: add
path: /machine/sysctls
value:
net.core.bpf_jit_harden: "1"
Контролплейны
controlplane.yaml
---
apiVersion: controlplane.cluster.x-k8s.io/v1alpha3
kind: TalosControlPlane
metadata:
name: k8s-cluster-control-plane
namespace: kube-system
labels:
cluster.x-k8s.io/cluster-name: k8s-cluster
spec:
version: v1.30.0
replicas: 3
infrastructureTemplate:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
name: k8s-cluster-control-plane
namespace: kube-system
controlPlaneConfig:
controlplane:
generateType: controlplane
strategicPatches:
- |
- op: replace
path: /machine/install
value:
disk: /dev/vda
- op: add
path: /machine/kubelet/extraArgs
value:
cloud-provider: external
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OpenStackMachineTemplate
metadata:
name: k8s-cluster-control-plane
namespace: kube-system
spec:
template:
spec:
flavor: m1.medium # МОЖЕТ ОТЛИЧАТЬСЯ #
image:
filter:
name: talos # МОЖЕТ ОТЛИЧАТЬСЯ #
sshKeyName: pgp-card # МОЖЕТ ОТЛИЧАТЬСЯ #
rootVolume:
sizeGiB: 50
type: __SSD__ # МОЖЕТ ОТЛИЧАТЬСЯ #
Применяем
kubectl apply -f .
Мигрируем контроль в таргетный кубер
В openstack уже появится новая сеть. ее надо подключить к темповому куберу чтобы он смог настроить все ноды. Чтобы получить kubeconfig нужно выполнить команду
clusterctl get kubeconfig k8s-cluster -n kube-system > k8s_kubeconfig
Дальше для таргетного кластера k8s_kubeconfig
аналогично статье:
Установка Cloud Provider OpenStack
Для того, чтобы наш кластер kubernetes мог ходить в OpenStack и создавать там диски или балансировщики, необходимо поставить Cloud Provider OpenStack. Там есть совершенно разные плагины, лично меня интересовал только OpenStack Cloud Controller Manager для создания балансировщиков и Cinder CSI Plugin, чтобы при создании PV, диски сами создавались в OpenStack и монтировались к нужному узлу.
Для работоспособности необходимо создать файл cloud-config с конфигурацией и кредами от OpenStack
[Global]
auth-url=OS_AUTH_URL
username=OS_USERNAME
password=OS_PASSWORD
tenant-name=OS_PROJECT_NAME
domain-name=default
project-domain-name=OS_PROJECT_DOMAIN_ID
user-domain-name=OS_USER_DOMAIN_ID
region=OS_REGION_NAME
[LoadBalancer]
lb-version=v2
subnet-id=<subnet_id> # ID подсети, из которой балансировщик будет выделять IP
floating-network-id=<floating_network_id> # ID сети в которой существует вышеуказанная подсеть
flavor-id=<flavor_id> # ID тарифа балансировщика
member-subnet-id=<member_subnet_id> # ID сети, в которой будут создаваться member. В моем случае это подсеть сети talos-k8s.
create-monitor=true
monitor-delay=5s
monitor-timeout=3s
monitor-max-retries=3
[BlockStorage]
trust-device-path=false
ignore-volume-az=true
OpenStack Cloud Controller Manager
Чтобы поставить этот модуль достаточно выполнить команды из документации.
Создать секрет из файла cloud-config описанного выше.
kubectl create secret -n kube-system generic cloud-config --from-file=cloud.conf
Пометить namespace для необходимых прав доступа, создать RBAC ресурсы и openstack-cloud-controller-manager daemonset.
kubectl label namespace kube-system pod-security.kubernetes.io/enforce=privileged kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/cloud-controller-manager-roles.yaml kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/cloud-controller-manager-role-bindings.yaml kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/openstack-cloud-controller-manager-ds.yaml
Проверяем статус что кластру хорошо и он готов к миграции
clusterctl describe cluster k8s-cluster -n kube-system
clusterctl move -n kube-system --to-kubeconfig ./k8s_kubeconfig
Готово!
Удаляем временую вм.
Теперь можно создавать узлы в Kubernetes просто патчами сущностей MachineDeployment
и в узком пуле железа создавать другие кластеры.
Для удобства записал видео гайд.
Спасибо за внимание