k3s, k0s, MicroK8s и лёгкие дистрибутивы Kubernetes: где они выигрывают
В предыдущей статье — https://courses.digitable.life/post/devops/06-kubernetes/ — мы разобрали Kubernetes целиком: apiserver, etcd, контроллеры, сеть, хранилища, автоскейлинг. И почти наверняка у вас осталось ощущение, что это очень много машинерии для того, чтобы запустить пять сервисов.
Ощущение верное. Kubernetes спроектирован Google под задачу «десятки тысяч узлов, тысячи команд, мультитенантность, любое облако». Если у вас три виртуалки в Hetzner и один продукт, вы платите полную цену за архитектуру, рассчитанную на другой масштаб.
Лёгкие дистрибутивы — это ответ на вопрос: можно ли получить настоящий Kubernetes API, но без цены за масштаб, которого у нас нет? Ответ — да, в существенной степени. Эта статья про то, как именно это сделано, чем за это платят и в какой момент экономия перестаёт окупаться.
Сразу зафиксирую главное, чтобы дальше читать без иллюзий: k3s — это не «упрощённый Kubernetes» и не «Kubernetes для игрушек». Это CNCF-сертифицированный дистрибутив, проходящий тот же conformance-набор тестов, что и EKS. Ваши манифесты, Helm-чарты, операторы и kubectl работают в нём без единого изменения. Отличается упаковка и эксплуатация, а не API.
Из чего складывается «вес» ванильного Kubernetes
Прежде чем убирать, надо понять, что именно весит. У обычной установки через kubeadm вес складывается из четырёх слагаемых, и они очень разные по природе.
1. Количество процессов. Control plane — это пять отдельных бинарников: kube-apiserver, kube-controller-manager, kube-scheduler, etcd, плюс на каждом узле kubelet и kube-proxy. Каждый — свой процесс со своим Go-рантаймом, своим heap, своим набором информеров, кэширующих состояние кластера в памяти. На простаивающем кластере они вместе занимают порядка 700–900 МБ RSS. Не потому что делают работу, а потому что существуют.
2. etcd. Отдельная распределённая база на Raft с жёсткими требованиями к диску. etcd делает fsync на каждую запись в WAL, и если p99 fsync превышает ~10 мс, кластер начинает терять лидера и «моргать». Это делает etcd практически несовместимым с дешёвыми сетевыми дисками и SD-картами. Плюс дефрагментация, компакция, снапшоты — отдельный класс эксплуатационных задач.
3. Дистрибутивная обвязка. Ванильный kubeadm даёт вам кластер без CNI, без Ingress, без LoadBalancer, без storage-класса, без metrics-server. Всё это надо выбрать, установить, обновлять и чинить. Это не байты — это человеко-часы, и они дороже байтов.
4. Легаси в самом бинарнике. Исторически kube-controller-manager и kubelet тащили in-tree код всех облачных провайдеров (AWS, Azure, GCE, OpenStack, vSphere) и всех storage-плагинов. Большая часть этого кода никогда не выполняется на вашем кластере, но лежит в бинарнике и увеличивает поверхность атаки.
Вот примерно так распределяется бюджет памяти на маленьком узле:
На узле с 32 ГБ разница в 350 МБ — статистический шум. На Raspberry Pi 4 с 2 ГБ или на самой дешёвой облачной виртуалке это разница между «влезает рабочая нагрузка» и «не влезает».
Что именно делает k3s: четыре решения
k3s появился в феврале 2019 года в Rancher Labs, в 2020-м передан в CNCF (сейчас — проект уровня Sandbox, k3s.io). Название — шутка: «Kubernetes» сокращают до «k8s», k3s вдвое меньше, значит пять букв вместо восьми.
Технически это четыре независимых решения, и полезно понимать каждое отдельно — потому что конкуренты повторяют их выборочно.
apiserver + cm + scheduler
+ kubelet + kube-proxy"] B2["kine → SQLite
(или etcd, или Postgres/MySQL)"] B3["containerd встроен"] B4["Flannel (VXLAN) из коробки"] B5["Traefik как Ingress"] B6["ServiceLB (Klipper) для LoadBalancer"] B7["local-path-provisioner"] B8["CoreDNS + metrics-server + Helm-controller"] end A1 & A2 & A3 & A5 & A6 --> B1 A4 --> B2 A7 --> B4 A8 --> B5 A9 --> B6 A10 --> B7 style B1 fill:#2a9d8f,color:#fff style B2 fill:#3d8bcd,color:#fff style B5 fill:#e9a23b,color:#fff style B6 fill:#e9a23b,color:#fff style B7 fill:#e9a23b,color:#fff
Решение 1: всё в одном процессе
Все компоненты control plane и узла слинкованы в один Go-бинарник и запускаются как горутины в одном процессе. Это убирает дублирование Go-рантайма, общие библиотеки грузятся один раз, а взаимодействие компонентов идёт через loopback вместо реального TCP.
Плата: нельзя обновить только scheduler; нельзя горизонтально масштабировать apiserver отдельно от controller-manager; падение одного компонента роняет процесс целиком (правда, systemd его тут же поднимает).
Решение 2: kine — этот трюк главный
kine («Kine Is Not Etcd») — shim, который реализует gRPC-интерфейс etcd поверх обычной SQL-базы: SQLite, PostgreSQL, MySQL, а также NATS. apiserver не знает, что говорит не с etcd.
Почему это работает? Потому что apiserver использует лишь узкое подмножество семантики etcd: диапазонные чтения по ключу, compare-and-swap по ревизии, watch с определённой ревизии. Kine реализует это одной таблицей вида «(id, name, created, deleted, prev_revision, value)», где id — монотонный счётчик, играющий роль etcd-ревизии, а watch — это опрос новых строк.
Практический эффект для одноузлового кластера огромен: вместо распределённого консенсуса на Raft с fsync на каждую запись — одиночный файл SQLite. Ниже latency, ниже потребление, никакой дефрагментации, бэкап — это cp файла.
Практический предел тоже понятен: watch через периодический опрос таблицы масштабируется хуже нативного etcd. Kine отлично держит десятки узлов и тысячи объектов, и начинает буксовать там, где в кластере идёт непрерывный высокочастотный поток изменений объектов.
Решение 3: вырезано легаси
Из бинарника убраны in-tree облачные провайдеры, in-tree storage-плагины, альфа-фичи и устаревшие API. Работает только CSI и внешние cloud-controller-manager — то есть ровно то, куда upstream и так двигался. Побочный эффект: если ваш чарт полагается на in-tree провижнер вроде kubernetes.io/aws-ebs, он не заведётся.
Решение 4: батарейки в комплекте
Это, по моему опыту, недооценённая часть. k3s из коробки даёт CNI, Ingress, DNS, metrics-server, StorageClass и — что редко бывает — работающий type: LoadBalancer на голом железе (компонент ServiceLB, он же Klipper: на каждом узле поднимается DaemonSet с host-портом и пробрасывает трафик в сервис).
Для маленькой команды это экономит не мегабайты, а недели: не нужно выбирать между Calico/Cilium/Flannel, разбираться с MetalLB в L2-режиме и настраивать nginx-ingress до первого рабочего HTTPS.
Установка и рабочие конфиги k3s
Минимум, который реально работает:
# server (control plane + рабочий узел), последняя стабильная версия канала
curl -sfL https://get.k3s.io | sh -
# проверяем
sudo k3s kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# node-1 Ready control-plane,master 24s v1.31.4+k3s1
# kubeconfig лежит здесь (root-only по умолчанию)
sudo cat /etc/rancher/k3s/k3s.yaml
Токен для подключения агентов:
sudo cat /var/lib/rancher/k3s/server/node-token
# K10a3f...::server:9d8c7b...
# на агенте
curl -sfL https://get.k3s.io | K3S_URL=https://10.0.0.10:6443 \
K3S_TOKEN=K10a3f...::server:9d8c7b... sh -
Но в проде так не ставят. Конфигурацию держат в файле, а не в аргументах юнита — тогда её можно версионировать и раскатывать Ansible-ом (https://courses.digitable.life/post/devops/10-configuration-management/) или cloud-init.
# /etc/rancher/k3s/config.yaml на server-узле
write-kubeconfig-mode: "0640"
tls-san:
- k8s.internal.acme.dev # имя, по которому ходит kubeconfig
- 10.0.0.100 # VIP балансировщика control plane
node-taint:
- "CriticalAddonsOnly=true:NoExecute" # не пускаем прикладные поды на control plane
# выключаем то, что заменим своим
disable:
- traefik # ставим ingress-nginx или Traefik своим чартом
- servicelb # вместо него MetalLB в BGP-режиме
disable-cloud-controller: true
# сеть
cluster-cidr: "10.42.0.0/16"
service-cidr: "10.43.0.0/16"
flannel-backend: "vxlan" # host-gw быстрее, но требует L2-связности узлов
# HA на встроенном etcd
cluster-init: true # ТОЛЬКО на первом server-узле
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 20
etcd-s3: true
etcd-s3-bucket: acme-k3s-backups
etcd-s3-region: eu-central-1
etcd-s3-endpoint: s3.eu-central-1.amazonaws.com
kube-apiserver-arg:
- "audit-log-path=/var/log/k3s-audit.log"
- "audit-log-maxage=30"
kubelet-arg:
- "system-reserved=cpu=200m,memory=256Mi"
- "eviction-hard=memory.available<200Mi,nodefs.available<10%"
Второй и третий server присоединяются так же, но с server: https://10.0.0.10:6443 вместо cluster-init.
Важная деталь про disable: отключать компоненты нужно до первого запуска. Если вы стартовали k3s с Traefik, а потом добавили его в disable, k3s удалит HelmChart-ресурс, но следы могут остаться. Чистый путь — k3s-uninstall.sh и переустановка, что на этапе bootstrap стоит две минуты.
Автодеплой манифестов и Helm-контроллер
k3s следит за каталогом /var/lib/rancher/k3s/server/manifests/: всё, что туда положено, применяется автоматически при старте. Это готовый механизм bootstrap без внешнего GitOps — им удобно поднимать первый слой (ArgoCD, который дальше подтянет остальное; про это — https://courses.digitable.life/post/devops/08-helm-and-gitops/).
Плюс встроенный Helm-контроллер, который умеет ставить чарты по CRD:
# /var/lib/rancher/k3s/server/manifests/argocd.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: argo-cd
namespace: kube-system
spec:
repo: https://argoproj.github.io/argo-helm
chart: argo-cd
version: 7.7.7
targetNamespace: argocd
createNamespace: true
valuesContent: |-
configs:
params:
server.insecure: true # TLS терминируется на Ingress
controller:
resources:
requests: {cpu: 250m, memory: 512Mi}
Классическая ошибка: править эти файлы «руками» на узле, а потом удивляться, что изменения откатились. Файлы в manifests/ — источник истины, k3s их переприменяет. Меняйте файл, не объект в кластере. И не редактируйте traefik.yaml — для этого есть HelmChartConfig:
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
ports:
web:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
Приватный реестр и air-gap
Отдельная сильная сторона k3s — работа в закрытом контуре. Настройка реестров лежит в одном файле и подхватывается встроенным containerd:
# /etc/rancher/k3s/registries.yaml
mirrors:
docker.io:
endpoint:
- "https://registry.internal.acme.dev"
"*":
endpoint:
- "https://registry.internal.acme.dev"
configs:
"registry.internal.acme.dev":
auth:
username: robot$k3s-puller
password: "${REGISTRY_TOKEN}"
tls:
ca_file: /etc/ssl/certs/acme-internal-ca.pem
Полностью офлайновая установка — это k3s-airgap-images-amd64.tar.zst в /var/lib/rancher/k3s/agent/images/ плюс бинарник и install-скрипт с INSTALL_K3S_SKIP_DOWNLOAD=true. Для промышленных объектов и госконтуров это часто главный аргумент за k3s.
Топологии: где на самом деле проходит граница между dev и продом
Самая частая ошибка в лёгких дистрибутивах — считать одноузловой кластер продом, потому что «он же работает».
Отдельно про HA: cluster-init со встроенным etcd требует нечётного числа server-узлов. Два server-узла — строго хуже одного: кворум Raft для двух членов равен двум, значит падение любого узла останавливает запись в кластер. Это ровно та ошибка, которую делают, «добавляя резервный мастер».
Как агент присоединяется к кластеру
Понимание bootstrap-протокола экономит часы при отладке «агент не появляется в get nodes».
K10<хэш CA>::server:<секрет> A->>S: POST /v1-k3s/serving-kubelet.crt
Basic auth: node:<секрет> S->>K: проверка токена, запись node-password S-->>A: клиентские сертификаты kubelet и kube-proxy A->>C: старт containerd, разворачивание образов из /agent/images A->>S: kubelet регистрирует Node-объект S->>K: запись Node S-->>A: watch: назначенные поды Note over A,S: дальше — обычный Kubernetes:
kubelet тянет спеки, containerd запускает контейнеры
Практические следствия:
- Токен состоит из хэша CA и секрета. Если вы переустановили server, сохранив старый токен на агентах, — хэш не сойдётся, и агент откажется подключаться с ошибкой про CA. Лечится удалением
/var/lib/rancher/k3s/agent/на агенте. - k3s запоминает пароль узла в секрете
<имя-узла>.node-password.k3sв namespacekube-system. Пересоздали виртуалку с тем же hostname — получите отказ регистрации. Либо удаляйте секрет, либо давайте уникальные имена узлам (а лучше — задавайте--with-node-id). - Порт 6443 нужен всем агентам; для встроенного etcd между server-узлами дополнительно 2379/2380, для Flannel VXLAN — UDP 8472.
k0s: другой набор компромиссов
k0s от Mirantis появился в конце 2020 года и решает ту же задачу иначе. Тоже один бинарник, тоже «zero friction», но:
- Никаких вырезаний из upstream. k0s собирает ванильные компоненты Kubernetes без патчей. Для тех, кому важна аудируемость и «мы точно на upstream» — это аргумент.
- Чёткое разделение controller и worker. По умолчанию controller-узел не запускает kubelet вообще — он не является узлом кластера. Это идеологически ближе к managed-кластерам и чище с точки зрения безопасности.
- Хранилище: встроенный etcd для HA или kine для SQL-бэкендов. То есть kine здесь тоже есть, но не по умолчанию для многоузловых конфигураций.
- k0sctl — декларативный инструмент развёртывания по SSH. Это, пожалуй, главное практическое отличие: один YAML описывает весь кластер, включая обновления.
# k0sctl.yaml — весь кластер в одном файле
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: acme-prod
spec:
hosts:
- ssh: {address: 10.0.0.11, user: root, keyPath: ~/.ssh/id_ed25519}
role: controller
- ssh: {address: 10.0.0.12, user: root, keyPath: ~/.ssh/id_ed25519}
role: controller
- ssh: {address: 10.0.0.13, user: root, keyPath: ~/.ssh/id_ed25519}
role: controller
- ssh: {address: 10.0.0.21, user: root, keyPath: ~/.ssh/id_ed25519}
role: worker
- ssh: {address: 10.0.0.22, user: root, keyPath: ~/.ssh/id_ed25519}
role: worker
k0s:
version: v1.31.4+k0s.0
config:
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
network:
provider: calico # или kuberouter
podCIDR: 10.244.0.0/16
telemetry: {enabled: false}
extensions:
helm:
repositories:
- name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
charts:
- name: ingress-nginx
chartname: ingress-nginx/ingress-nginx
version: "4.11.3"
namespace: ingress-nginx
k0sctl apply --config k0sctl.yaml # развернуть или обновить весь кластер
k0sctl kubeconfig --config k0sctl.yaml > ~/.kube/config
Отдельно стоит упомянуть Autopilot — встроенный в k0s механизм автообновлений через CRD Plan, который сам кордонит, дренирует, обновляет и возвращает узлы в строй. У k3s аналог существует, но как отдельный проект (system-upgrade-controller).
Ещё одна интересная вещь из мира k0s — k0smotron: он запускает control plane кластеров как поды внутри другого Kubernetes-кластера. Это «control plane as a workload», удобно для сценария «сотни маленьких кластеров для клиентов/филиалов/edge». Тот же паттерн у Kamaji и Cluster API.
MicroK8s: путь Canonical
MicroK8s — от Canonical, поставляется как snap-пакет. Философия — «всё через аддоны»:
sudo snap install microk8s --classic --channel=1.31/stable
sudo microk8s enable dns storage ingress metallb:10.0.0.100-10.0.0.110
sudo microk8s enable hostpath-storage observability
sudo microk8s kubectl get pods -A
# HA: добавить узлы
sudo microk8s add-node
# на втором узле: microk8s join 10.0.0.11:25000/abcdef...
Сильные стороны: богатая витрина аддонов (включая Istio, Knative, Kubeflow, GPU-операторы), автоматический переход в HA-режим при трёх и более узлах, отличная интеграция с Ubuntu.
Слабые — оборотная сторона snap. Snap обновляется автоматически внутри канала; на проде это надо явно ограничивать (snap refresh --hold), иначе однажды кластер обновится сам, ночью, без вас. Confinement snap-а иногда конфликтует с нестандартными путями и сторонними CSI. И зависимость от snapd делает MicroK8s неудобным вне Ubuntu-мира.
Для локальной разработки на Ubuntu-ноутбуке и для университетских/лабораторных сценариев MicroK8s очень хорош. Для гетерогенного парка серверов — k3s или k0s предсказуемее.
Остальная поляна: не путайте классы инструментов
Здесь часто возникает каша, поэтому разложу по назначению.
Для локальной разработки (кластер живёт часы):
- kind — Kubernetes in Docker: узлы это контейнеры. Мгновенное создание/удаление, идеален для CI, поддерживает мультиузловые конфигурации и загрузку локальных образов
kind load. - k3d — то же самое, но узлы — контейнеры с k3s. Стартует ещё быстрее kind, встроенный registry одной командой.
- minikube — самый старый, поддерживает много драйверов (VM, Docker, Podman) и аддоны. Тяжелее, но универсальнее.
Для edge/прода:
- RKE2 (он же RKE Government) — тоже от Rancher/SUSE. Это, по сути, «k3s с приоритетом безопасности»: отдельные процессы вместо одного, только etcd, соответствие CIS Benchmark и FIPS 140-2, поддержка SELinux из коробки. Берут туда, где есть регуляторные требования.
- Talos Linux — самое радикальное решение: это не дистрибутив Kubernetes, а иммутабельная ОС, спроектированная только под Kubernetes. Нет SSH, нет shell, нет пакетного менеджера — управление только через gRPC API и декларативный machine config. Прекрасен для флотов и для тех, кто хочет исключить «дрейф» узлов как класс. Порог входа выше: привычные способы «зайти и посмотреть» не работают.
Сравнение: честная таблица
| k3s | k0s | MicroK8s | RKE2 | Talos | kubeadm | Managed (EKS/GKE/AKS) | |
|---|---|---|---|---|---|---|---|
| Дистрибуция | 1 бинарник, install.sh | 1 бинарник, k0sctl | snap | 1 бинарник | образ ОС | пакеты + образы | — |
| Память control plane | ~450 МБ | ~500 МБ | ~700 МБ | ~800 МБ | ~700 МБ | ~800 МБ | не ваша забота |
| Хранилище по умолчанию | SQLite (kine) | etcd | dqlite | etcd | etcd | etcd | управляемое |
| Альтернативные бэкенды | etcd, Postgres, MySQL, NATS | etcd, kine | — | — | — | — | — |
| Батарейки из коробки | CNI, Ingress, LB, SC, DNS | CNI, Helm-расширения | аддоны | CNI, Ingress | CNI | ничего | зависит |
| ARM / Raspberry Pi | отлично | хорошо | хорошо | да | да | со сборкой | нет |
| Air-gap | отлично | хорошо | средне | отлично | хорошо | вручную | нет |
| Обновления | скрипт / system-upgrade-controller | k0sctl / Autopilot | snap refresh | скрипт | machine config | вручную по узлам | кнопка |
| CIS / FIPS | частично | частично | нет | да | да | вручную | да |
| Порог входа | низкий | низкий | очень низкий | средний | высокий | высокий | низкий |
| Эксплуатационная нагрузка | низкая | низкая | низкая | средняя | низкая после освоения | высокая | минимальная |
| Реалистичный потолок | ~50–100 узлов | сотни | десятки | сотни | сотни | тысячи | тысячи |
| Стоимость лицензий | 0 | 0 | 0 | 0 | 0 | 0 | 70 $–75/мес за кластер |
Числа по памяти — порядок величин на простаивающем кластере, они зависят от версии и включённых компонентов. Проверяйте на своём железе: kubectl top nodes и systemd-cgtop.
Теперь то же самое в виде карты выбора:
Стоимость: где лёгкий дистрибутив реально выигрывает деньгами
Здесь важно считать всё, а не только строчку в счёте. Возьмём типовую задачу: прод небольшого SaaS — 3 узла по 2 vCPU / 4 ГБ, отказоустойчивый control plane, один публичный балансировщик.
| Вариант | Control plane | Узлы | Балансировщик | Сеть/прочее | Итого в месяц |
|---|---|---|---|---|---|
| EKS + 3× t3.medium (us-east-1) | ~73 $ | ~91 $ | ~18 $ (NLB) | ~33 $ NAT Gateway + EBS ~15 $ | ~230 $ |
| GKE Standard + 3× e2-medium | ~73 $ (сверх free tier) | ~73 $ | ~18 $ | ~10 $ | ~174 $ |
| AKS Free tier + 3× B2s | 0 $ | ~88 $ | ~18 $ | ~10 $ | ~116 $ |
| DigitalOcean DOKS + 3 узла | 0 $ | ~72 $ | ~12 $ | входит | ~84 $ |
| k3s на 3× Hetzner CAX21 (ARM) | входит в узлы | ~€15 | ~€6 | входит | ~€21 (~23 $) |
| k3s на 1× Hetzner CX32 (dev/staging) | входит | ~€7 | — | входит | ~€7 (~8 $) |
Разброс — десятикратный. Но это ещё не вся правда, потому что мы не посчитали людей.
Грубая, но рабочая модель полной стоимости владения:
TCO ≈ (стоимость инфраструктуры) + (часы эксплуатации в месяц) × (стоимость часа инженера)
Прикинем при ставке 50 $/час:
| Вариант | Инфраструктура | Часов/мес | Стоимость труда | TCO/мес |
|---|---|---|---|---|
| EKS | 230 $ | 2 | 100 $ | 330 $ |
| DOKS | 84 $ | 3 | 150 $ | 234 $ |
| k3s на Hetzner, 3 узла HA | 23 $ | 6 | 300 $ | 323 $ |
| kubeadm на Hetzner, 3 узла | 23 $ | 14 | 700 $ | 723 $ |
Отсюда три честных вывода, которые редко произносят вслух:
- На проде среднего размера managed-кластер часто не дороже самоуправляемого — вы платите деньгами вместо часов, и это выгодная сделка, если ваши инженеры делают продукт, а не инфраструктуру.
- Лёгкие дистрибутивы выигрывают именно на краях распределения: когда кластеров много и они крошечные (edge, филиалы, устройства), либо когда бюджет мал абсолютно (pet-проекты, стартап до раунда, dev/staging-среды, учебные стенды).
- kubeadm вручную не выигрывает почти никогда. Если вы выбрали self-hosted, выбирайте дистрибутив с готовыми обновлениями.
Особенно нагляден случай «много маленьких кластеров»: 200 магазинов сети, в каждом — сервер с k3s. Managed-вариант тут просто не существует (нет облака в магазине), а 73 $/мес за control plane × 200 — это 175 тыс $яч в год за то, чего вы не можете использовать.
Подробнее про экономику инфраструктуры — https://courses.digitable.life/post/devops/15-cloud-cost-and-tradeoffs/ и https://courses.digitable.life/post/devops/14-vps-vds-and-bare-metal/.
Эксплуатация: обновления, бэкапы, восстановление
Обновления через system-upgrade-controller
Ручное обновление узлов не масштабируется дальше пяти штук. У k3s для этого есть system-upgrade-controller, работающий по декларативным Plan:
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: server-plan
namespace: system-upgrade
spec:
concurrency: 1 # по одному server за раз — иначе потеряете кворум
cordon: true
nodeSelector:
matchExpressions:
- {key: node-role.kubernetes.io/control-plane, operator: In, values: ["true"]}
serviceAccountName: system-upgrade
upgrade:
image: rancher/k3s-upgrade
version: v1.31.4+k3s1
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: agent-plan
namespace: system-upgrade
spec:
concurrency: 2
cordon: true
drain:
force: true
skipWaitForDeleteTimeout: 60
nodeSelector:
matchExpressions:
- {key: node-role.kubernetes.io/control-plane, operator: DoesNotExist}
prepare:
image: rancher/k3s-upgrade
args: ["prepare", "server-plan"] # ждём, пока обновятся server-узлы
serviceAccountName: system-upgrade
upgrade:
image: rancher/k3s-upgrade
version: v1.31.4+k3s1
Жизненный цикл узла при таком обновлении:
или под без контроллера Stuck --> Drained: force + skipWaitForDeleteTimeout Drained --> Upgrading: подмена бинарника, рестарт systemd-юнита Upgrading --> Rejoining: kubelet регистрируется с новой версией Rejoining --> Uncordoned: kubectl uncordon Uncordoned --> Ready: следующий узел Upgrading --> Failed: узел не поднялся Failed --> [*]: ручное вмешательство,
откат из снапшота etcd
Два правила, которые спасают:
concurrency: 1для server-узлов всегда. Обновление двух из трёх одновременно — потеря кворума etcd и остановка кластера.- Kubernetes не поддерживает перескок через минорную версию. С 1.29 на 1.31 идти нужно через 1.30.
Planэтого за вас не проверит.
Бэкап и восстановление
Для встроенного etcd:
# ручной снапшот
sudo k3s etcd-snapshot save --name pre-upgrade
# список (включая уехавшие в S3)
sudo k3s etcd-snapshot ls
# восстановление: на ПЕРВОМ server, при остановленных остальных
sudo systemctl stop k3s
sudo k3s server \
--cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/pre-upgrade-165...
sudo systemctl start k3s
# затем на остальных server-узлах: удалить /var/lib/rancher/k3s/server/db и перезапустить
Для SQLite всё проще и это отдельный плюс одноузловых инсталляций:
sudo systemctl stop k3s
sudo tar czf /backup/k3s-$(date +%F).tar.gz \
/var/lib/rancher/k3s/server/db \
/var/lib/rancher/k3s/server/token \
/var/lib/rancher/k3s/server/tls
sudo systemctl start k3s
Ключевой момент, который забывают: снапшот базы бесполезен без каталога tls/ и токена. Восстановите базу с другими CA-сертификатами — и ни один агент не подключится.
Отдельно: бэкап state кластера не равен бэкапу данных приложений. local-path-provisioner в k3s кладёт PV в обычный каталог на конкретном узле, без репликации и без снапшотов. Для stateful-нагрузок на self-hosted-кластере берите Longhorn, OpenEBS или CSI внешнего хранилища, а бэкап делайте Velero.
Terraform-развёртывание k3s на дешёвых VPS
Показательный пример того, как весь HA-кластер описывается в IaC (подробнее — https://courses.digitable.life/post/devops/09-terraform-and-iac/):
resource "hcloud_server" "k3s_server" {
count = 3
name = "k3s-server-${count.index + 1}"
server_type = "cax21" # 4 vCPU ARM, 8 ГБ — ~€7/мес
image = "ubuntu-24.04"
location = "fsn1"
ssh_keys = [hcloud_ssh_key.ops.id]
network { network_id = hcloud_network.private.id }
user_data = templatefile("${path.module}/cloud-init.yaml", {
k3s_version = var.k3s_version
k3s_token = var.k3s_token # из хранилища секретов, не из tfvars в git
is_first = count.index == 0
server_url = "https://10.0.1.10:6443"
})
}
resource "hcloud_load_balancer_service" "k8s_api" {
load_balancer_id = hcloud_load_balancer.cp.id
protocol = "tcp"
listen_port = 6443
destination_port = 6443
health_check {
protocol = "tcp"
port = 6443
interval = 10
timeout = 5
retries = 3
}
}
# firewall: apiserver только из приватной сети и с бастиона
resource "hcloud_firewall" "k3s" {
name = "k3s-internal"
rule {
direction = "in"
protocol = "tcp"
port = "6443"
source_ips = ["10.0.1.0/24", var.bastion_cidr]
}
rule {
direction = "in"
protocol = "udp"
port = "8472" # Flannel VXLAN
source_ips = ["10.0.1.0/24"]
}
}
Типичные ошибки
Собрано из практики и багтрекеров — почти всё это встречается регулярно.
- Считать одноузловой k3s продом. Он замечательно работает ровно до первого перезапуска узла. Если бизнес зависит от сервиса — три server-узла или managed-кластер.
- Два server-узла «для надёжности». Кворум Raft для двух членов равен двум: вы получили систему, которая ломается вдвое чаще одиночной. Только нечётное число.
- Ожидать HA от SQLite. SQLite-бэкенд принципиально одноузловой. Многоузловой HA — это встроенный etcd или внешняя СУБД, других вариантов нет.
- etcd на медленном диске. Сетевой том с p99
fsyncв 30 мс превращает кластер в мигающую гирлянду: лидер переизбирается, apiserver таймаутит, узлы уходят в NotReady. Для встроенного etcd нужен локальный SSD/NVMe. Симптом ищите в логах:apply request took too long,leader changed. - Не отключить Traefik/ServiceLB перед установкой своего Ingress. Два ingress-контроллера, дерущиеся за 80/443 на host-портах, дают увлекательную отладку.
- Править ресурсы, созданные автодеплоем. k3s переприменит файл из
manifests/и затрёт правки. ИспользуйтеHelmChartConfigили отключайте компонент. - Не заметить, что MicroK8s обновился сам. Snap-канал
1.31/stableобновляется по патч-версиям автоматически. На проде:sudo snap refresh --hold microk8sи обновляйтесь осознанно. - Пересоздать узел с тем же hostname. Секрет
<node>.node-password.k3sостался в кластере — регистрация будет отклонена. Удалите секрет или используйте уникальные имена. - Забыть про cgroup v2 на одноплатниках. На Raspberry Pi OS нужно дописать в
/boot/cmdline.txt:cgroup_memory=1 cgroup_enable=memory, иначе kubelet не стартует. - Использовать
local-pathдля stateful-данных без бэкапа. ЭтоhostPathс человеческим лицом: узел умер — данные умерли вместе с ним. - Перепрыгивать минорные версии при обновлении. С 1.29 сразу на 1.31 — сломанный кластер. Только последовательно.
- Хранить kubeconfig с адресом
127.0.0.1. k3s по умолчанию пишет туда localhost. Для внешнего доступа нуженtls-sanс реальным именем/VIP на этапе установки — добавление SAN потом требует пересоздания сертификатов. - Не ограничить ресурсы системных компонентов. Без
system-reservedиeviction-hardпрожорливый под на маленьком узле уронит kubelet, и узел уйдёт в NotReady вместе со всеми соседями. - Игнорировать бэкап
tls/и токена. База без сертификатов не восстанавливается в рабочий кластер.
Как это выглядит в проде: три сценария
Розничная сеть, 200 точек. На каждой точке — промышленный ПК с k3s в одноузловом режиме, локальные сервисы (касса, весы, синхронизация). Управление — Fleet или ArgoCD из центра: один Git-репозиторий, 200 подписчиков. Обновления идут волнами по 10 % точек. Никакого HA на точке нет и не нужно: если сервер магазина умер, магазин всё равно не работает. Ключевые требования — air-gap, автоперезапуск, размер образа для доставки по узкому каналу.
SaaS-стартап, 8 инженеров. Прод — managed-кластер (DOKS/GKE), потому что дежурить по etcd некому. Staging и preview-среды — k3s на паре дешёвых VPS, разворачиваются Terraform-ом за 4 минуты. CI поднимает эфемерные кластеры через k3d прямо в раннере для интеграционных тестов: это дешевле и быстрее, чем держать общий dev-кластер, и тесты изолированы.
Промышленное предприятие, закрытый контур. RKE2 вместо k3s: требования CIS Benchmark, SELinux в enforcing, FIPS-криптография, полный air-gap с внутренним Harbor. Обновления — раз в квартал, по регламенту, из подписанного офлайн-бандла.
Обратите внимание: во всех трёх случаях лёгкий дистрибутив выбран не «потому что модно» и не «потому что дёшево», а потому что конкретное ограничение (нет облака / нет людей / нет интернета) делало альтернативу неприменимой.
Когда лёгкий дистрибутив — неправильный ответ
Честности ради, границы применимости:
- Больше ~100 узлов или высокая частота изменений объектов. Kine и одиночный процесс перестают быть преимуществом; берите RKE2, kubeadm или managed.
- Мультитенантность с недоверенными нагрузками. Нужны сетевые политики, изоляция control plane, аудит — здесь выигрывают managed-кластеры и RKE2.
- Жёсткая привязка к облачным сервисам. Если вам нужны IRSA, Workload Identity, нативные ALB Ingress Controller и CSI облака — self-hosted-кластер придётся долго доводить до того же уровня интеграции.
- Нет никого, кто дежурит. Кластер, который вы подняли за 30 секунд, придётся чинить не за 30 секунд. Про дежурства и наблюдаемость — https://courses.digitable.life/post/devops/16-observability-and-oncall/.
Мини-итог
- Лёгкие дистрибутивы — это тот же Kubernetes API (CNCF-conformant), другая упаковка и эксплуатация. Манифесты и чарты переносятся без изменений.
- k3s экономит за счёт четырёх решений: один процесс, kine поверх SQL, вырезанное легаси, батарейки в комплекте. Главный технический трюк — kine.
- k0s даёт ванильные компоненты, чистое разделение controller/worker и k0sctl как декларативный деплой; MicroK8s — самый низкий порог входа в Ubuntu-мире ценой особенностей snap.
- Граница dev/прод проходит не по дистрибутиву, а по топологии: одноузловой кластер — не прод, два server-узла хуже одного.
- Деньгами лёгкие дистрибутивы выигрывают на краях: очень много крошечных кластеров или очень маленький бюджет. В середине managed-кластер часто дешевле по TCO, потому что вы платите деньгами вместо часов.
- Эксплуатация решается заранее: декларативные обновления (
Plan/Autopilot), снапшоты etcd вместе сtls/и токеном, отдельный бэкап данных приложений.
Источники
- k3s documentation — установка, HA-режимы, air-gap,
config.yaml, восстановление кластера. - kine — реализация etcd API поверх SQL; в README описана схема таблицы и ограничения.
- k0s documentation и k0sctl — декларативное развёртывание и Autopilot.
- MicroK8s docs — аддоны, HA на dqlite, управление каналами snap.
- RKE2 documentation — CIS Hardening Guide и FIPS-сборки.
- Talos Linux docs — иммутабельная ОС под Kubernetes, machine config.
- kind и k3d — эфемерные кластеры для разработки и CI.
- CNCF Certified Kubernetes Conformance — список сертифицированных дистрибутивов и сам тестовый набор.
- etcd: Hardware recommendations — почему требования к диску настолько строгие.
- system-upgrade-controller — CRD
Planи модель автообновления узлов. - Kubernetes version skew policy — что можно и нельзя при обновлении.
- Longhorn и Velero — реплицируемое хранилище и бэкапы для self-hosted-кластеров.
Что дальше
Кластер поднят и обновляется сам. Дальше — вопрос, как в него попадает прикладной софт: описывать релизы шаблонами, накладывать оверлеи на среды и превратить Git в единственный источник истины о состоянии кластера. Об этом — следующая статья: Helm, Kustomize и GitOps: ArgoCD и Flux.