CI/CD, инфраструктура и облака k3s, k0s, MicroK8s и лёгкие дистрибутивы Kubernetes: где они выигрывают
0%

k3s, k0s, MicroK8s и лёгкие дистрибутивы Kubernetes: где они выигрывают

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-плагинов. Большая часть этого кода никогда не выполняется на вашем кластере, но лежит в бинарнике и увеличивает поверхность атаки.

Вот примерно так распределяется бюджет памяти на маленьком узле:

Бюджет памяти узла на 2 ГБ при разных дистрибутивах Kubernetes

На узле с 32 ГБ разница в 350 МБ — статистический шум. На Raspberry Pi 4 с 2 ГБ или на самой дешёвой облачной виртуалке это разница между «влезает рабочая нагрузка» и «не влезает».

Что именно делает k3s: четыре решения

k3s появился в феврале 2019 года в Rancher Labs, в 2020-м передан в CNCF (сейчас — проект уровня Sandbox, k3s.io). Название — шутка: «Kubernetes» сокращают до «k8s», k3s вдвое меньше, значит пять букв вместо восьми.

Технически это четыре независимых решения, и полезно понимать каждое отдельно — потому что конкуренты повторяют их выборочно.

Решение 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 и продом

Самая частая ошибка в лёгких дистрибутивах — считать одноузловой кластер продом, потому что «он же работает».

Четыре топологии кластера k3s

Отдельно про HA: cluster-init со встроенным etcd требует нечётного числа server-узлов. Два server-узла — строго хуже одного: кворум Raft для двух членов равен двум, значит падение любого узла останавливает запись в кластер. Это ровно та ошибка, которую делают, «добавляя резервный мастер».

Как агент присоединяется к кластеру

Понимание bootstrap-протокола экономит часы при отладке «агент не появляется в get nodes».

Практические следствия:

  • Токен состоит из хэша CA и секрета. Если вы переустановили server, сохранив старый токен на агентах, — хэш не сойдётся, и агент откажется подключаться с ошибкой про CA. Лечится удалением /var/lib/rancher/k3s/agent/ на агенте.
  • k3s запоминает пароль узла в секрете <имя-узла>.node-password.k3s в namespace kube-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 $

Отсюда три честных вывода, которые редко произносят вслух:

  1. На проде среднего размера managed-кластер часто не дороже самоуправляемого — вы платите деньгами вместо часов, и это выгодная сделка, если ваши инженеры делают продукт, а не инфраструктуру.
  2. Лёгкие дистрибутивы выигрывают именно на краях распределения: когда кластеров много и они крошечные (edge, филиалы, устройства), либо когда бюджет мал абсолютно (pet-проекты, стартап до раунда, dev/staging-среды, учебные стенды).
  3. 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

Жизненный цикл узла при таком обновлении:

Два правила, которые спасают:

  • 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"]
  }
}

Типичные ошибки

Собрано из практики и багтрекеров — почти всё это встречается регулярно.

  1. Считать одноузловой k3s продом. Он замечательно работает ровно до первого перезапуска узла. Если бизнес зависит от сервиса — три server-узла или managed-кластер.
  2. Два server-узла «для надёжности». Кворум Raft для двух членов равен двум: вы получили систему, которая ломается вдвое чаще одиночной. Только нечётное число.
  3. Ожидать HA от SQLite. SQLite-бэкенд принципиально одноузловой. Многоузловой HA — это встроенный etcd или внешняя СУБД, других вариантов нет.
  4. etcd на медленном диске. Сетевой том с p99 fsync в 30 мс превращает кластер в мигающую гирлянду: лидер переизбирается, apiserver таймаутит, узлы уходят в NotReady. Для встроенного etcd нужен локальный SSD/NVMe. Симптом ищите в логах: apply request took too long, leader changed.
  5. Не отключить Traefik/ServiceLB перед установкой своего Ingress. Два ingress-контроллера, дерущиеся за 80/443 на host-портах, дают увлекательную отладку.
  6. Править ресурсы, созданные автодеплоем. k3s переприменит файл из manifests/ и затрёт правки. Используйте HelmChartConfig или отключайте компонент.
  7. Не заметить, что MicroK8s обновился сам. Snap-канал 1.31/stable обновляется по патч-версиям автоматически. На проде: sudo snap refresh --hold microk8s и обновляйтесь осознанно.
  8. Пересоздать узел с тем же hostname. Секрет <node>.node-password.k3s остался в кластере — регистрация будет отклонена. Удалите секрет или используйте уникальные имена.
  9. Забыть про cgroup v2 на одноплатниках. На Raspberry Pi OS нужно дописать в /boot/cmdline.txt: cgroup_memory=1 cgroup_enable=memory, иначе kubelet не стартует.
  10. Использовать local-path для stateful-данных без бэкапа. Это hostPath с человеческим лицом: узел умер — данные умерли вместе с ним.
  11. Перепрыгивать минорные версии при обновлении. С 1.29 сразу на 1.31 — сломанный кластер. Только последовательно.
  12. Хранить kubeconfig с адресом 127.0.0.1. k3s по умолчанию пишет туда localhost. Для внешнего доступа нужен tls-san с реальным именем/VIP на этапе установки — добавление SAN потом требует пересоздания сертификатов.
  13. Не ограничить ресурсы системных компонентов. Без system-reserved и eviction-hard прожорливый под на маленьком узле уронит kubelet, и узел уйдёт в NotReady вместе со всеми соседями.
  14. Игнорировать бэкап 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.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов