Скрытый control plane в k8s

от автора

Введение

Давайте рассмотрим нестандартный вариант установки Kubernetes, а именно установим кластер без control-plane ноды как таковой в стандартном ее восприятии. Сделаем некий аналог установки кластера Kubernetes где слой control-plane будет просто хостом без CRI, только базове systemd сервисы на поддержке одной команды, а worker ноды предоставляются как сервис командам без прямого взаимодействия этих команд с управляющим слоем. Для реализации возьмем 3 сервера.

Стенд

Три ВМ в VirtualBox (host-only сеть ‘192.168.56.0/24’):

Роль

Hostname

IP

Что крутится

Master

node1

192.168.56.230

etcd, apiserver, controller-manager, scheduler (+ kubectl)

Worker

node2

192.168.56.231

containerd, kubelet, kube-proxy, Cilium

Worker

node3

192.168.56.232

containerd, kubelet, kube-proxy, Cilium

ОС: Ubuntu 20.04 2 CPU, 4 Gb Ram, 30 Gb HDD

Версии компонентов для установки:

  • Kubernetes v1.30.14

  • etcd v3.5.21

  • containerd 1.7.27

  • Cilium v1.16.10

  • CoreDNS v1.11.3

Сеть кластера:

Схема лабораторного стенда

Схема лабораторного стенда

Процесс установки

Для начала необходимо подготовить сервера. Тут все по классике отключаем фаил подкачки, включаем модуля, настраиваем sysctl и ставим необходимые пакеты.

# На каждой ноде (под sudo):swapoff -ased -i '/ swap /s/^/#/' /etc/fstabcat >/etc/modules-load.d/k8s.conf <<EOFoverlaybr_netfilterEOFmodprobe overlaymodprobe br_netfiltercat >/etc/sysctl.d/99-kubernetes.conf <<EOFnet.bridge.bridge-nf-call-iptables  = 1net.bridge.bridge-nf-call-ip6tables = 1net.ipv4.ip_forward                 = 1EOFsysctl --systemapt-get updateapt-get install -y apt-transport-https ca-certificates curl gnupg \  conntrack ipset iptables socat nfs-commonsystemctl disable --now ufw || true

После чего на машине, с которой будет происходить установка, необходимо скачать необходимые бинарники: apiserver, controller-manager, scheduler, kubelet, kube-proxy, kubectl, etcd, containerd.

K8S_VER=v1.30.14ETCD_VER=v3.5.21CONTAINERD_VER=1.7.27RUNC_VER=v1.2.6CNI_VER=v1.6.2curl -LO https://dl.k8s.io/${K8S_VER}/kubernetes-server-linux-amd64.tar.gzcurl -LO https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD\\_VER}-linux-amd64.tar.gzcurl -LO https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VER}/containerd-${CONTAINERD\\_VER}-linux-amd64.tar.gzcurl -LO https://github.com/opencontainers/runc/releases/download/${RUNC_VER}/runc.amd64curl -LO https://github.com/containernetworking/plugins/releases/download/${CNI_VER}/cni-plugins-linux-amd64-${CNI\\_VER}.tgzcurl -LO https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.30.1/crictl-v1.30.1-linux-amd64.tar.gz

На ней же мы генерируем необходимые сертификаты. В принципе тут сложного быть ничего не должно, все это подробно и не один раз описывалось в статьях Kubernetes the Hard Way по этому смысла особенно зацикливаться на данных вопросах я не вижу.

А вот дальше начинается самое интересное… На сервер с control-plane мы ставим ‘etcd’, ‘etcdctl’, ‘kube-apiserver’, ‘kube-controller-manager’, ‘kube-scheduler’, ‘kubectl’ а так же копируем необходимые сертификаты. На worker ноды мы устанавливаем ‘containerd’, ‘containerd-shim-runc-v2’, ‘ctr’, ‘runc’, ‘crictl’, ‘kubelet’, ‘kube-proxy’ (ставил я его как systemd сервис и при установке cilium не менял kube-proxy. Тут каждый может на будущее решить что ему нужно, но в рамках лабораторного стенда вопрос не принципиальный).

И после запуска сервисов на сервере с control-plane и worker нодах вы получаете кластер Kubernetes, выполнив к которому команду kubectl get nodes вы получите в выводе только worker ноды.

kubectl get nodesNAME    STATUS   ROLES    AGE   VERSIONnode2   Ready    worker   12h   v1.30.14node3   Ready    worker   12h   v1.30.14kubectl get pods -ANAMESPACE     NAME                        READY   STATUS    RESTARTS  AGEkube-system   cilium-7wmn4                1/1     Running   0         12hkube-system   cilium-operator-66d-wb6x2   1/1     Running   0         12hkube-system   cilium-vcpg2                1/1     Running   0         12hkube-system   coredns-8df57d68f-gzbx9     1/1     Running   0         12hkube-system   coredns-8df57d68f-hks76     1/1     Running   0         12h

Почему же так получается? Регистрацию ноды в Kubernetes API выполняет kubelet а не kubeadm. Kubeadm только выпускает токен на основании которого kubelet запрашивает постоянный сертификат. Регистрация ноды — создание объекта Node. Когда у kubelet есть сертификат, он делает следующее:

  • Отправляет запрос на регистрацию (если —register-node=true, что по умолчанию).

  • Передаёт данные о ноде: имя, IP, CPU, RAM, ОС, версии kubelet/container runtime, метки (labels), тэйнты (taints) и т.п.

  • API-сервер создаёт объект Node (или обновляет существующий, если имя уже занято).

С этого момента control plane знает о ноде и ее видно в выводе команды kubectl get nodes. Ну и так как мы не ставили на сервер с компонентами control plane kubelet соответственно мы и не регистрировали данный сервер как ноду kubernetes кластра. Возникает вопрос как же тогда удалить worker ноду из кластера, ведь если мы удалим ноду командой kubectl delete node и перезапустим kubelet нода снова появится в кластере? Ответ так же прост. После удаления из кластера worker ноду необходимо остановить kubelet или отозвать ранее выданный сертификат то бы никто не смог запустить новую ноду и не украсть ваш workload.

Нюанс для тех, кто будет повторять в данном сетапе

  1. Устанавливал cilium через cilium-cli:

cilium install --version 1.16.10 \ --set ipam.mode=kubernetes \--set kubeProxyReplacement=false \--set hubble.enabled=false \--set envoy.enabled=false \  --set bpf.preallocateMaps=false

Почему так:

kubeProxyReplacement=false - kube-proxy остаётся systemd-сервисом на воркерах.Hubble/Envoy/preallocateMaps - экономия RAM на нодах.

Через несколько минут:

DaemonSet cilium     2/2 Readycilium-operator      1/1 ReadyNodes                Ready

2. При старте CoreDNS с фиксированным IP в сервисе совпадающим с тем, что прописан в kubelet получаю ошибку вида

[FATAL] plugin/loop: Loop ... detected for zone "."

Почему так происходит? На хостах ‘/etc/resolv.conf’ смотрит в systemd-resolved (‘127.0.0.53’). CoreDNS с плагином ‘loop’ обнаруживает петлю и умирает.

Фикс для лабы: убрать ‘loop’ из Corefile и форвардить наружу явно:

forward . 8.8.8.8 1.1.1.1

После рестарта Deployment — обе реплики Running, ‘nslookup kubernetes.default’ из busybox отдаёт ‘10.96.0.1’, внешний DNS тоже отвечает.

Что же дальше?

Вариант дальнейшего развития

Вариант дальнейшего развития

А дальше самое интересное. Подобные реализации помогают разграничивать зоны ответственности в кластере, выдавать заказчику только worker ноды без доступа к управляющему слою Kubernetes, защищают от ряда CVE например CVE-2023-5408 и другие.

В плане развития — в будущем сервер с control plane может спокойно оказаться не отдельным или отдельными серверами, а всего лишь подами в неком кластере Kubernetes который будет управлять ими (и в котором по мимо данного кластера могут в отдельных неймспейсах храниться другие поды которые будут являться control plane других кластеров), в случае необходимости скейлить api, автоматизировать выдачу сертификатов и многое другое. Дальше все зависит от целей, задач и полета фантазий.

ссылка на оригинал статьи https://habr.com/ru/articles/1061228/