Настройка Kubernetes

Серверы подготовлены. Теперь из них создаётся единый Kubernetes-кластер. Сначала запускается управляющий узел и внутренняя сеть, затем подключаются рабочие узлы, настраиваются внешний IP-адрес, HTTP-доступ, поддержка GPU и доступ к закрытому реестру контейнерных образов.

На этой странице

2.1. Создание управляющего узла Kubernetes

Создаётся управляющий узел кластера, настраивается доступ kubectl и параметры журналирования kubelet. До установки сетевого компонента Cilium состояние NotReady является ожидаемым.

Инициализация кластера

Только на управляющем узле Kubernetes:

MASTER_IP="${CONTROL_PLANE_IP}"
POD_NETWORK="10.244.0.0/16"
SERVICE_NETWORK="10.16.0.0/16"
K8S_VERSION="v1.35.6"

kubeadm config images pull \
  --kubernetes-version "${K8S_VERSION}" \
  --cri-socket unix:///run/containerd/containerd.sock
kubeadm init \
  --control-plane-endpoint "${MASTER_IP}:6443" \
  --apiserver-advertise-address "${MASTER_IP}" \
  --skip-phases="addon/kube-proxy" \
  --pod-network-cidr="${POD_NETWORK}" \
  --service-cidr="${SERVICE_NETWORK}" \
  --cri-socket="unix:///run/containerd/containerd.sock" \
  --kubernetes-version "${K8S_VERSION}"

Рабочие узлы на этом этапе ещё не подключаются.

Настройка kubectl

mkdir -p /root/.kube
cp /etc/kubernetes/admin.conf /root/.kube/config
chmod 600 /root/.kube/config

До установки Cilium управляющий узел ожидаемо NotReady.

Журналирование kubelet

На управляющем узле:

kubectl get cm -n kube-system kubelet-config -o json | sed '
  s|cluster.local\\n|cluster.local\\ncontainerLogMaxFiles: 2\\n|g
  s|cluster.local\\n|cluster.local\\ncontainerLogMaxSize: 10Mi\\n|g
' | kubectl replace -f -

echo "containerLogMaxFiles: 2" >> /var/lib/kubelet/config.yaml
echo "containerLogMaxSize: 10Mi" >> /var/lib/kubelet/config.yaml
systemctl restart kubelet

Важно: после установки каждого системного компонента необходимо дождаться завершения запуска и убедиться, что его Pod-ы находятся в состоянии Running/Ready. Только после этого переходить к следующему компоненту.

2.2. Настройка сети кластера с помощью Cilium

Cilium обеспечивает сетевое взаимодействие между Pod-ами и сервисами Kubernetes. После успешного запуска Cilium управляющий узел должен перейти в состояние Ready.

Используем Cilium 1.16.5.

helm repo add cilium https://helm.cilium.io/
helm repo update

CILIUM_VERSION="1.16.5"
API_SERVER_HOST="${CONTROL_PLANE_IP}"
API_SERVER_PORT="6443"
helm upgrade --install cilium cilium/cilium \
  --version "${CILIUM_VERSION}" \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost="${API_SERVER_HOST}" \
  --set k8sServicePort="${API_SERVER_PORT}" \
  --set operator.replicas=1 \
  --set ipam.mode=kubernetes \
  --set hubble.enabled=false \
  --set l7Proxy=false

Проверка:

kubectl -n kube-system get pods -o wide
kubectl get nodes -o wide

Ожидается:

${CONTROL_PLANE_NODE}   Ready

2.3. Подключение рабочих узлов

После готовности сети к кластеру подключаются узел инфраструктурных сервисов и узел с GPU. Затем им назначается метка для размещения прикладных компонентов.

После того как Cilium установлен и ${CONTROL_PLANE_NODE} перешёл в Ready, подключаем рабочие узлы.

На управляющем узле получить актуальную команду подключения:

kubeadm token create --print-join-command

На рабочих узлах выполнить полученную команду подключения:

kubeadm join ${CONTROL_PLANE_IP}:6443 \
  --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

Включить kubelet:

systemctl enable kubelet

Проверка выполняется с управляющего узла:

kubectl get nodes -o wide

Ожидается:

${CONTROL_PLANE_NODE}    Ready   control-plane   v1.35.6
${DB_NODE}        Ready   <none>          v1.35.6
${GPU_NODE}    Ready   <none>          v1.35.6

Проверить Cilium на всех узлах:

kubectl -n kube-system get ds cilium
kubectl -n kube-system get pods -l k8s-app=cilium -o wide

Метки для прикладных сервисов:

kubectl label node "${DB_NODE}" unica-role=app --overwrite
kubectl label node "${GPU_NODE}" unica-role=app --overwrite

2.4. MetalLB

Установка MetalLB

В локальной инфраструктуре нет облачного балансировщика нагрузки. MetalLB предоставляет сервисам Kubernetes внешний IP-адрес из физической сети.

Используем:

MetalLB 0.14.9
L2 mode
FRR disabled
один выделенный VIP /32

Добавить репозиторий Helm:

helm repo add metallb https://metallb.github.io/metallb
helm repo update

Зафиксировать:

METALLB_VERSION="0.14.9"

Создать пространство имён:

kubectl create namespace metallb-system

Разрешить для компонентов MetalLB режим с расширенными системными правами:

kubectl label namespace metallb-system \
  pod-security.kubernetes.io/enforce=privileged \
  pod-security.kubernetes.io/audit=privileged \
  pod-security.kubernetes.io/warn=privileged

Установить MetalLB без FRR:

helm upgrade --install metallb metallb/metallb \
  --namespace metallb-system \
  --version "${METALLB_VERSION}" \
  --set speaker.frr.enabled=false \
  --set frrk8s.enabled=false

Проверить:

kubectl -n metallb-system get pods -o wide

Ожидается:

metallb-controller   Running
metallb-speaker      Running   ${CONTROL_PLANE_NODE}
metallb-speaker      Running   ${DB_NODE}
metallb-speaker      Running   ${GPU_NODE}

Для стенда используется один заранее выделенный свободный IP-адрес. Сетевой интерфейс определяется автоматически по маршруту до этого адреса.

Для каждого стенда используется выделенный свободный IP из физической сети.

Для текущей конфигурации:

# METALLB_IP задаётся на шаге 1.1

Перед использованием необходимо убедиться, что IP свободен и не выдаётся DHCP.

Определение сетевого интерфейса

Имя сетевого интерфейса не задаётся вручную и не переносится из конфигурации другой площадки. Он определяется по маршруту до выбранного VIP:

METALLB_INTERFACE=$(
  ip route get "${METALLB_IP}" |
  awk '{for(i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
)

Проверить:

echo "${METALLB_IP}"
echo "${METALLB_INTERFACE}"

Для текущей конфигурации ожидается:

${METALLB_IP}
eth0

На всех Kubernetes-узлах необходимо убедиться, что сеть VIP доступна через интерфейс с тем же именем:

ip route get "${METALLB_IP}"

Создание пула IP-адресов MetalLB

Пул определяет, какой внешний IP-адрес MetalLB может назначать сервисам типа LoadBalancer. Для данного стенда используется один заранее выделенный адрес ${METALLB_IP}.

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: unica-pool
  namespace: metallb-system
spec:
  addresses:
    - ${METALLB_IP}/32
EOF

Настройка объявления адреса в сети L2

L2Advertisement разрешает MetalLB объявлять выделенный IP-адрес в локальной сети через найденный ранее сетевой интерфейс. После этой настройки адрес сможет использоваться сервисом ingress-nginx.

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: main
  namespace: metallb-system
spec:
  ipAddressPools:
    - unica-pool
  interfaces:
    - ${METALLB_INTERFACE}
EOF

Проверить:

kubectl -n metallb-system get ipaddresspool
kubectl -n metallb-system get l2advertisement

Для текущей конфигурации:

unica-pool -> ${METALLB_IP}/32
main       -> unica-pool / eth0

Важно: сам по себе ping VIP не является проверкой MetalLB. VIP начинает использоваться после появления Service type=LoadBalancer.

2.5. ingress-nginx

ingress-nginx принимает входящие HTTP-запросы на внешнем IP-адресе MetalLB и передаёт их сервисам внутри Kubernetes. После установки проверяется весь сетевой путь до контроллера.

Версии

Используются:

пакет Helm ingress-nginx 4.12.1
контроллер v1.12.1

Ingress запускается только на:

${GPU_NODE}

Пометить узел для ingress-nginx

ingress-nginx запускается только на ${GPU_NODE}. Метка ingress используется как селектор узла при установке контроллера.

kubectl label node ${GPU_NODE} ingress=

Проверить:

kubectl get node ${GPU_NODE} --show-labels | grep ingress

Добавить репозиторий Helm

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

Используем версию 4.12.1:

INGRESS_NGINX_VERSION="4.12.1"
# METALLB_IP задаётся на шаге 1.1

Установка ingress-nginx

Внешний доступ идёт через MetalLB.

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --version "${INGRESS_NGINX_VERSION}" \
  --set controller.kind=DaemonSet \
  --set-string 'controller.nodeSelector.ingress=' \
  --set controller.service.enabled=true \
  --set controller.service.type=LoadBalancer \
  --set controller.service.externalTrafficPolicy=Local \
  --set-string 'controller.service.annotations.metallb\.io/address-pool=unica-pool' \
  --set-string "controller.service.annotations.metallb\.io/loadBalancerIPs=${METALLB_IP}" \
  --set controller.config.body-size=50m \
  --set controller.config.hsts=false \
  --set controller.config.large-client-header-buffers="4 32k" \
  --set controller.config.proxy-body-size=1024m \
  --set controller.config.proxy-buffer-size=128k \
  --set controller.config.proxy-buffers="4 256k" \
  --set controller.config.proxy-busy-buffers-size=256k \
  --set controller.config.proxy-connect-timeout="15" \
  --set controller.config.proxy-read-timeout="300" \
  --set controller.config.proxy-send-timeout="300" \
  --set controller.config.server-name-hash-bucket-size="256" \
  --set controller.config.worker-shutdown-timeout=10s

externalTrafficPolicy=Local сохраняет исходный IP клиента и, при использовании MetalLB в режиме L2 ограничивает обработку внешнего трафика узлами, на которых запущен локальный экземпляр ingress-nginx.

Проверка внешнего HTTP

До создания прикладных Ingress-ресурсов nginx-controller обычно отвечает стандартным 404, что достаточно для проверки сетевого пути:

curl -I --connect-timeout 5 "http://${METALLB_IP}"

Получение HTTP-ответа от ingress-nginx подтверждает прохождение трафика по цепочке:

client
  -> ${METALLB_IP}
  -> MetalLB L2
  -> ${GPU_NODE}
  -> ingress-nginx

2.6. NVIDIA GPU Operator

На стенде используется NVIDIA A100 PCIe 40GB, установленная на узле ${GPU_NODE}.

Драйвер NVIDIA и NVIDIA Container Toolkit не устанавливаются отдельными пакетами операционной системы. GPU Operator разворачивает драйвер в виде системного компонента Kubernetes, загружает необходимые модули на GPU-узле и управляет остальными компонентами NVIDIA. Поэтому наличие утилит NVIDIA непосредственно в пользовательском окружении хоста не требуется.

До установки GPU Operator достаточно убедиться, что операционная система видит устройство NVIDIA:

lspci -nn | grep -i nvidia

Команда должна вывести устройство NVIDIA. До установки GPU Operator драйвер NVIDIA на узле не установлен, поэтому nvidia-smi на хосте не используется.

Добавление репозитория Helm NVIDIA

На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

Пространство имён GPU Operator

kubectl create namespace gpu-operator

GPU Operator требует запуска компонентов с расширенными системными правами:

kubectl label --overwrite namespace gpu-operator \
  pod-security.kubernetes.io/enforce=privileged

Установка NVIDIA GPU Operator

Используем GPU Operator v25.10.1 и драйвер NVIDIA 580.126.09:

GPU_OPERATOR_VERSION="v25.10.1"
NVIDIA_DRIVER_VERSION="580.126.09"

Установка:

helm upgrade --install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --version "${GPU_OPERATOR_VERSION}" \
  --set driver.enabled=true \
  --set driver.version="${NVIDIA_DRIVER_VERSION}" \
  --set driver.kernelModuleType=auto \
  --set driver.usePrecompiled=false \
  --set toolkit.enabled=true \
  --set nfd.enabled=true \
  --set dcgmExporter.enabled=false \
  --wait

GPU Operator разворачивает и сопровождает на GPU-узле следующие компоненты:

NVIDIA GPU Driver
NVIDIA Container Toolkit
NVIDIA Kubernetes Device Plugin
GPU Feature Discovery
Node Feature Discovery
MIG Manager
Validator

Проверка NVIDIA GPU Operator

После установки убедитесь, что компоненты GPU Operator запущены, а Kubernetes видит GPU-ресурсы узла:

kubectl -n gpu-operator get pods -o wide
kubectl get node "${GPU_NODE}" -o json | \
  jq '.status.allocatable | with_entries(select(.key | startswith("nvidia.com")))'

Поскольку драйвер развёрнут через GPU Operator, проверка выполняется внутри Pod-а драйвера:

DRIVER_POD=$(kubectl -n gpu-operator get pod \
  -l app=nvidia-driver-daemonset \
  -o jsonpath='{.items[0].metadata.name}')

kubectl -n gpu-operator exec "${DRIVER_POD}" -c nvidia-driver-ctr -- nvidia-smi

Команда должна показать установленный драйвер и обнаруженную GPU. Отсутствие nvidia-smi в командной оболочке самого хоста при такой схеме не является ошибкой.

2.7. Доступ к закрытому реестру контейнерных образов

Прикладные образы и пакеты Helm находятся в закрытом реестре. Для доступа нужны адрес реестра, имя пользователя и пароль. Эти значения используются дважды: containerd — для загрузки контейнерных образов, Helm — для загрузки OCI-пакетов.

Подставьте выданные для стенда учётные данные:

REGISTRY="<registry-host>"
REGISTRY_USERNAME="<registry-user>"
REGISTRY_PASSWORD="<registry-password>"

Это переменные командной оболочки. Если настройка узлов и авторизация Helm выполняются в разных сеансах, задайте эти три переменные в каждом соответствующем сеансе перед выполнением команд ниже.

Настройка containerd

Настройку необходимо выполнить на узлах, на которых будут запускаться прикладные Pod-ы. В текущей схеме это ${DB_NODE} и ${GPU_NODE}.

На узле ${DB_NODE}:

cat >> /etc/containerd/config.toml <<EOF

[plugins."io.containerd.grpc.v1.cri".registry.configs."${REGISTRY}".auth]
  username = "${REGISTRY_USERNAME}"
  password = "${REGISTRY_PASSWORD}"
EOF

systemctl restart containerd
systemctl restart kubelet

На узле ${GPU_NODE} после установки NVIDIA GPU Operator:

cat >> /etc/containerd/conf.d/99-nvidia.toml <<EOF

[plugins."io.containerd.grpc.v1.cri".registry.configs."${REGISTRY}".auth]
  username = "${REGISTRY_USERNAME}"
  password = "${REGISTRY_PASSWORD}"
EOF

systemctl restart containerd
systemctl restart kubelet
crictl info | jq '.config.containerd.runtimes | keys'

Авторизация Helm

containerd и Helm используют независимую авторизацию. Поэтому на управляющем узле Kubernetes необходимо отдельно выполнить вход в реестр для Helm:

echo "${REGISTRY_PASSWORD}" | helm registry login "${REGISTRY}" \
  --username "${REGISTRY_USERNAME}" \
  --password-stdin

Проверить доступ к пакету Helm:

helm show chart oci://<REGISTRY>/<HELM_CHARTS_PATH>/rospartner-ai-files \
  --version <FILES_CHART_VERSION>

Если команда выводит метаданные пакета, авторизация выполнена успешно. Helm хранит данные авторизации локально на управляющем узле в файле ~/.config/helm/registry/config.json.

2.8. cert-manager

cert-manager устанавливается как системный компонент Kubernetes и используется для управления сертификатами, необходимыми прикладным ресурсам.

Версия:

cert-manager v1.20.3

На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):

CERT_MANAGER_VERSION="v1.20.3"

helm upgrade --install cert-manager \
  oci://quay.io/jetstack/charts/cert-manager \
  --version "${CERT_MANAGER_VERSION}" \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --set prometheus.enabled=false

Проверка:

kubectl -n cert-manager get pods -o wide

Ожидается:

cert-manager              1/1 Running
cert-manager-cainjector   1/1 Running
cert-manager-webhook      1/1 Running

Проверка Kubernetes-платформы

Перед установкой инфраструктурного ПО убедитесь, что все узлы находятся в состоянии Ready, сетевые компоненты запущены, внешний IP-адрес доступен, GPU виден Kubernetes, а cert-manager работает.

Выполнить на управляющем узле Kubernetes:

kubectl get nodes -o wide
kubectl -n kube-system get pods -l k8s-app=cilium -o wide
kubectl -n metallb-system get pods -o wide
kubectl -n ingress-nginx get pods,svc -o wide
kubectl -n gpu-operator get pods -o wide
kubectl -n cert-manager get pods -o wide

Ожидаемый результат: все узлы находятся в состоянии Ready, системные Pod-ы — в состоянии Running/Ready, а сервис ingress-nginx получил внешний IP-адрес ${METALLB_IP}.