Хабр, привет! Есть такой тип проектов, которые начинаются со слов «а что если» и заканчиваются тем, что ты в три часа ночи объясняешь kubelet-у, что таблица — это его новый control plane. Так вышло и тут, всё родилось из субботней шутки в одном телеграм-канале по кубернетесу.

Правила игры простые: раз весь мир можно создать в эксельке, то пускай всё состояние кластера живёт внутри электронной таблицы. Не «метаданные в таблице», не «экспорт в CSV» — а буквально: Deployments, Nodes, Pods, Events — это вкладки Google Sheets (или листы Excel). Планировщик читает и пишет ячейки. kubectl-подобная утилита ходит в таблицу. И — вот это ключевое — на другом конце крутятся настоящие Docker-контейнеры. Таблица не симулирует кластер. Таблица и есть кластер.

Проект называется Sheeternetes. Это, конечно, шутка. Но шутка, которая компилируется, проходит тесты и переживает падение ноды.

Ниже — как это устроено, как это поднять у себя за пять минут, как оно работает на bare metal без интернета (в Excel и LibreOffice), как связать несколько таблиц-кластеров в федерацию с живой миграцией, почему контейнеры можно хранить прямо в ячейках, — и что это внезапно выросло в целый фонд с 30 проектами, стандартом контейнеров и системой сертификации.

Дисклеймер на всякий случай

Не запускайте на этом прод. Если запустите — снимите на видео диалог сохранения. It reconciles.

Под капотом здесь планировщик с bin-packing и failover, HMAC-подписи, live-миграция make-before-break, свой формат образов. Просто субстрат выбран максимально еретический. Единственное жёсткое ограничение проекта, из которого растёт вся остальная эстетика: по возможности всё делать внутри таблиц.

Архитектура: таблица как источник истины

Kubernetes — это, если сильно упростить, «желаемое состояние в etcd + контроллеры, которые сводят реальность к желаемому». Замените etcd на вкладку Google Sheets, а контроллер — на Apps Script и bash-демон, и вы получите Sheeternetes.

Архитектура Sheeternetes
Архитектура Sheeternetes

Компоненты:

  • Таблица — durable store и single source of truth. Четыре вкладки: Deployments, Nodes, Pods, Events. Строка = объект, шапка = схема.

  • apiserver — процесс, который отдаёт данные по HTTP и запускает планировщик. В облачной версии это Google Apps Script, привязанный к таблице; в on-prem — маленький Python-сервер поверх .xlsx.

  • kubeletkubelet.sh, обычный bash-скрипт. Превращает любой Docker-хост в «ноду»: шлёт heartbeat в apiserver, получает список подов, которые должен запустить, и сводит локальный Docker к этому списку.

  • skctl — крошечный kubectl. curl + jq поверх контракта apiserver.

Контракт между всеми частями предельно简单:

GET  ?token=<T>&kind=pods|nodes|deployments|events   ->  { "items": [ ... ] }
POST { "token": T, "action": "apply|scale|delete", ... }
POST { "token": T, "node": ..., "pods": [...] }         # heartbeat кубелета -> desired pods

Вот и вся «плоскость управления». Дальше — только детали, которых, как обычно, вагон.

Пять минут до первого кластера

Разберём облачный вариант — на Google Sheets. Нужны Google-аккаунт, Docker-хост, bash, curl, jq. Весь код — в репозитории github.com/sncfoundation/sheeternetes.

Шаг 1. Control plane. Шаблона копировать не нужно — его намеренно нет: пустая таблица превращается в кластер сама.

  1. Создаём пустую Google-таблицу.

  2. Extensions → Apps Script, вставляем Code.gs из репозитория.

  3. В начале файла ставим свой TOKEN.

  4. Один раз запускаем setup() — он превращает пустой лист в полную структуру: четыре вкладки с заголовками, пара примеров-деплойментов и reconcile-триггер раз в минуту.

  5. Deploy → New deployment → Web app (Execute as: Me, Who has access: Anyone), копируем /exec URL.

Хочется сперва просто посмотреть на живой кластер, ничего не поднимая? На сайте есть референсное демо на трёх настоящих таблицах — открываете и читаете running-state прямо в ячейках.

Шаг 2. Поднимаем ноду — на любой машине с Docker:

export WEBAPP_URL=https://script.google.com/macros/s/XXXX/exec
export TOKEN=secret
NODE_NAME=node-a CPU_TOTAL=4000 MEM_TOTAL=8192 ./kubelet.sh
# [kubelet] node=node-a ip=10.0.0.1 cpu=4000m mem=8192Mi -> ...

Нет трёх машин? ./local-cluster.sh поднимет node-a/b/c как фоновые кубелеты на одном Docker-демоне — удобно, чтобы посмотреть на планировщик и failover локально.

Шаг 3. Деплоим нагрузку. Манифест — обычный JSON:

{
  "deployments": [
    { "name": "web",   "image": "nginx:alpine",       "replicas": 2, "cpu_req": 100, "mem_req": 32 },
    { "name": "hello", "image": "hashicorp/http-echo", "replicas": 1, "cpu_req": 50,  "mem_req": 16,
      "command": "-text=hello-from-a-spreadsheet -listen=:8080" }
  ]
}
skctl apply lab/hello-web.json
skctl get pods

И вот тут начинается магия: вы открываете таблицу — и видите свой кластер как строки. Вкладка Deployments — это ваши деплойменты. Вкладка Pods — реально запущенные контейнеры с их нодами и статусами. Меняете replicas в ячейке руками — планировщик на следующем heartbeat досоздаёт поды. Таблица — это kubectl edit, только курсором.

Кластер как вкладки таблицы
Кластер как вкладки таблицы

А в терминале всё выглядит подозрительно похоже на «взрослый» оркестратор:

skctl в деле
skctl в деле
$ skctl get pods
NAME    DEPLOYMENT  NODE    PHASE    CONTAINER
web-1   web         node-a  Running  a1b2c3d4e5f6
web-2   web         node-b  Running  b2c3d4e5f6a7
hello-1 hello       node-a  Running  c3d4e5f6a7b8

Контейнеры — настоящие. docker ps на ноде покажет их. Sheetlium (наш «Cilium») заодно кладёт их в общую Docker-сеть и вешает на имя деплоймента network-alias — вот вам и Service c DNS.

Планировщик: не игрушечный

Самое частое возражение: «ну это же просто список, какой там планировщик». А вот и нет. На каждый heartbeat apiserver честно раскладывает реплики по нодам, и это чистая, покрытая юнит-тестами функция. Что она умеет:

  • Bin-packing по ресурсам. Под с cpu_req/mem_req кладётся на ноду, где есть место. Не влезает никуда — становится Unschedulable.

  • Sticky placement. Под не скачет между нодами без причины — остаётся там, где был, пока нода жива.

  • Failover. Нода перестала слать heartbeat дольше NODE_TTL (30с) — становится NotReady, её поды переезжают на живых.

  • cordon / drain / migrate. Ровно как в kubectl.

  • affinity + taints/tolerations. Полноценно: node_selector против лейблов ноды, NoSchedule-тейнты против толерейшенов.

Обслуживание ноды выглядит так:

skctl cordon node-a          # новые поды сюда не едут, текущие работают
skctl drain  node-a          # эвакуировать поды на других, затем cordon
skctl uncordon node-a        # вернуть в строй
skctl migrate web-1 node-b   # переselить конкретный под (make-before-break)
skctl label  node-a disk=ssd
skctl taint  node-a gpu=true:NoSchedule

Хотите закрепить GPU-нагрузку за нужными нодами? Обычный node_selector в манифесте:

{ "name": "ml-infer", "image": "sicf:tinyllm", "replicas": 1,
  "cpu_req": 800, "mem_req": 256, "node_selector": "accel=gpu", "tolerations": "gpu=true" }

Проверить, что failover работает, можно грубо: убиваете kubelet на node-a, ждёте TTL — и видите, как поды сами перечисляются на node-b. В таблице. В реальном времени. Это, честно, гипнотизирует.

SheetOS: операционка для ноды

Раз уж у нас есть оркестратор, ему нужна нода-ОС. Встречайте SheetOS — «Talos для таблиц»: минимальная, иммутабельная, конфигурируемая декларативно, и — самое важное — самораскатывающаяся.

Идея: sheetstrap берёт «образ» ноды из таблицы, поднимает поверх себя runtime, регистрируется как Node и — если сказать ему — сам ставит поверх себя Sheeternetes-кластер. То есть таблица-ОС бутстрапит таблицу-кластер. Матрёшка, которая при этом ещё и reconcile-ится.

# нода читает свой желаемый образ/конфиг из таблицы и собирает себя
sheetstrap up --config nodeconfig.json
# распределённый режим: несколько листов собираются в кластер по join-токену
sheetstrap join --token <...>

SheetOS отвечает на любимый вопрос интервью «а ваша ОС распределённая?» — да, и она умеет ставить оркестратор поверх себя же. Дальше рекурсию оставим на усмотрение читателя.

Bare metal: Sheeternetes без интернета, в Excel и LibreOffice

Облако — это прекрасно, но настоящая мужская дисциплина — это air-gapped on-prem на десктопной таблице. Для этого есть отдельная редакция — sheeternetes-onprem.

Здесь apiserver — это локальный Python-процесс поверх .xlsx, с тем же самым контрактом, что и облачный. То есть kubelet.sh и skctl цепляются к нему без единой правки. Интернет не нужен: таблица — хранилище, процесс — apiserver.

pip install openpyxl
cp .skctl.env.example .skctl.env      # WEBAPP_URL=http://localhost:8787, TOKEN=...

make up      # терминал 1: apiserver поверх cluster.xlsx на :8787
make node    # терминал 2: kubelet (этот хост становится нодой)
make apply   # терминал 3: применить lab/hello-web.json
make pods    # смотрим, как планировщик раскладывает и запускает поды

Всё, что нужно, лежит в одной репе: apiserver.py (control plane + планировщик), kubelet.sh, skctl, Makefile, лаб-манифест. Планировщик — тот же, что в облаке: bin-packing, failover, cordon/drain/migrate, affinity/taints. Мультинода — просто запускаете kubelet.sh на других машинах в LAN, указав им адрес apiserver.

Про Excel и LibreOffice честно: openpyxl читает .xlsx, поэтому в LibreOffice Calc делаем Save As → Excel 2007-365 (.xlsx). Нативный .ods через Python-UNO — в роадмапе. И да, у бэкендов есть жёсткие пределы (об этом ниже, когда дойдём до нагрузочного тестирования — спойлер: Excel начинает страдать задолго до формальных лимитов).

Вся эта редакция — с тестами (47 штук) и CI. Потому что «шутка» не значит «без тестов».

Федерация: связываем таблицы-кластеры между собой

Один кластер — это скучно. Настоящая цель — связать несколько таблиц в федерацию, в том числе разного типа: локальный Excel-кластер ↔ облачный Google Sheets-кластер.

On-prem нельзя достучаться снаружи, поэтому мост bridge.py звонит наружу сам: читает локальный apiserver и сводит его с удалённым пиром (Google Sheets-кластером или другой нодой). Получаем cross-substrate service discovery.

# федеративная картина сервисов (+ --push публикует локальные деплойменты пиру)
python3 bridge.py status  --local http://localhost:8787 --local-token secret \
                          --peer  https://script.google.com/macros/s/XXXX/exec --peer-token secret2

И — то, ради чего всё затевалось — живая миграция между субстратами, make-before-break:

python3 bridge.py migrate web --from local --to peer --rollback-window 30 \
                          --local http://localhost:8787 --local-token secret \
                          --peer  https://script.google.com/macros/s/XXXX/exec --peer-token secret2

Логика: скопировать спеку на target → дождаться, пока там поднимется → только потом снять с source. Не поднялось — source остаётся нетронутым, простоя ноль. А --rollback-window ещё и последит за target после переключения и откатит на source, если тот деградировал.

Плюс двусторонняя синхронизация как демон:

python3 bridge.py sync --interval 60 --local ... --peer ...

И безопасность поверх этого — опциональные HMAC-подписи запросов. Поднимаете apiserver с SIGNING_KEY, и каждый POST обязан нести X-SNCF-Timestamp + X-SNCF-Signature в пределах TTL (защита от подделки и replay):

SIGNING_KEY=shared-secret WORKBOOK=cluster.xlsx python3 apiserver.py
python3 bridge.py sync --peer-signing-key shared-secret --local ... --peer ...

Да, у нас HMAC на таблицах. Мы тоже не до конца понимаем, как до этого дошли.

Два формата контейнеров: обычный и «нативный табличный»

Вот это, пожалуй, самая интересная часть. Sheeternetes работает с контейнерами в двух режимах, и они взаимозаменяемы.

1. OCI-режим (обычные контейнеры). В манифесте image: nginx:alpine — kubelet тянет образ из обычного реестра через Docker. Полная совместимость со всей экосистемой контейнеров, работает из коробки. То есть уже сегодня Sheeternetes гоняет самые настоящие OCI-образы — таблица держит только желаемое состояние, а байты образа приходят из Docker Hub.

2. SICF native — образ живёт прямо в таблице. image: sicf:web:v1 — и образ резолвится из табличного хранилища образов, без внешнего реестра. Как? Слои образа — это строки на вкладке Layers, разбитые на base64-чанки и адресуемые по sha256:-дайджесту; манифест — строка на вкладке Images. Puller собирает слои из ячеек, проверяет дайджест, разворачивает rootfs и запускает. Полностью air-gapped, «таблица — это реестр».

Между форматами — мост (sheetbuild), без потерь:

sheetbuild import nginx:alpine    # тянет OCI-образ и записывает его в таблицу как SICF
sheetbuild export web:v1 ghcr.io/you/web:v1   # обратно в OCI-реестр

Оба режима адресуют контент одними и теми же sha256:-дайджестами, поэтому import → export — это round-trip.

Есть, разумеется, физика. Google Sheets: 50 000 символов на ячейку и 10 млн ячеек на книгу. Поэтому нативный режим — для маленьких образов: статические Go/Rust-бинарники, scratch/alpine, и особенно WASM-модули (которые ложатся идеально). Многогигабайтный CUDA-образ — это OCI-режим с реестром, и точка.

Всё это оформлено не как «фича», а как стандарт — но об этом в главе про фонд.

Наблюдаемость: Prometheus и Grafana, но в ячейках

Куда же без дашбордов. Sheetheus скрейпит метрики (в таб Metrics), Sheetfana рисует их — прямо внутри таблицы, средствами условного форматирования и спарклайнов. Выглядит это неприлично прилично:

Дашборд Sheetfana
Дашборд Sheetfana

Панели «Pods Running», «Nodes Ready», «CPU Allocated», гистограмма подов по нодам, тайм-серия аллокации CPU — и всё это ячейки. SheetAIOps сверху смотрит на вкладку Events и делает вид, что предсказывает аномалии.

Sheet-Native Computing Foundation

В какой-то момент проектов стало так много, что они попросили себе фонд. Так появился SNCF — Sheet-Native Computing Foundation: вендор-нейтральный дом для «sheet-native computing», пародия на CNCF, которая… работает. За шуткой — реальный код: планировщик с failover, верифицируемая сертификация, живое демо на трёх настоящих Google-таблицах и собственный стандарт контейнеров.

Стандарт: Sheet Container Initiative (SCI)

Где две независимые реализации — там должна быть спецификация. У нас их две (Apps Script и Python), поэтому появился SCI — по образцу OCI, три спеки:

  • SCRI (Sheet Container Runtime Interface) — контракт apiserver↔kubelet. Уже v0.1, с уровнями conformance Core/Full.

  • SICF (Sheet-Native Image Container Format) — тот самый двухрежимный формат образов. Тоже v0.1.

  • SDS (Sheet Distribution Spec) — как образы ездят между таблицами и реестрами. В работе.

Спеки написаны с реального поведения (описывают то, что уже крутится), с нормативными MUST/SHOULD и исполняемым conformance-сьютом. Это, помимо прочего, честный учебник по тому, что такое CRI и OCI на самом деле.

Сертификация: с серийными номерами и рангами

Да, у нас есть верифицируемая сертификация инженеров. Открываете issue с формой экзамена → GitHub Action чеканит серийник (например, SFE000001) в публичный реестр. 19 программ (по одной на проект), у каждой свой префикс.

Сертификат CSFE
Сертификат CSFE

Собрал несколько сертификатов — можешь податься на ранг, и система проверит тебя по реестру:

  • 🚀 SheetCadet — 1 сертификат

  • 👨‍🚀 SheetAstronaut — 3

  • 🛰️ SheetCommander — 6

  • 🌌 SheetAdmiral — 10+

Ранг SheetAdmiral
Ранг SheetAdmiral

Всё проверяемо: у каждого сертификата и ранга есть серийник и запись в открытом реестре. Никакого «поверьте на слово» — только «сверьтесь с таблицей».

Другие проекты фонда

  • Sheeternetes — оркестрация контейнеров. Флагман.

  • SheetOS — нода-ОС (Talos).

  • sheeternetes-onprem — bare-metal редакция на Excel/LibreOffice + федерация.

  • Sheetlium — сеть (Cilium). Sheetmesh — service mesh (Istio).

  • Sheetlux CD — GitOps-доставка (Argo CD).

  • Sheetstor — распределённое блочное хранилище (LINSTOR).

  • SheetHub — DevOps-форж (GitLab). Уже сделан внешним контрибьютором — см. ниже.

  • Sheetheus / Sheetfana — метрики и дашборды (Prometheus/Grafana).

  • SheetFinOps — учёт стоимости инфраструктуры ($0, разумеется).

  • SheetAssembly — WASI/WASM-нагрузки. skctl-wasm — CLI в браузере.

  • Sheetelligence / SheetAIOps — LLM-оркестрация и AIOps.

  • Cloud connectors — запуск в managed-облаках (Cloud Run, ACI, Yandex, ECS…).

  • Data & Storage — Sheedis (Redis), Sheetgres (Postgres), MySheet (MySQL), SheetMongo (Mongo), Sheethouse (ClickHouse), Sheetka (Kafka), SheetWire (реальные клиенты БД к таблице).

  • Языковые клиенты — PowerShell, Lisp и, для отважных, Brainfuck.

  • SCI — стандарт контейнеров (SCRI/SICF/SDS).

Роадмап (кратко)

  • Sheeternetes: мульти-хост overlay-сеть, реальные Secrets (в скрытых листах), RBAC через protected ranges, HPA через условное форматирование, документированный DR через историю ревизий, conformance-сьют.

  • SheetVirt: аналог KubeVirt — VM как поды.

  • SICF/native: свой формат образов + нагрузочное тестирование бэкендов (сколько образов влезет в Excel, пока он не взмолится — отдельный открытый вопрос, которым мы всерьёз займёмся).

  • Безопасность: SheetFalco (runtime security), SheetSign (подпись образов), SheetPolicy (admission через формулы).

  • Экосистема миграции с настоящих платформ (импорт Kubernetes/Compose-манифестов).

Присоединяйтесь

Это open source, Apache 2.0, и он живой. Первый внешний контрибьютор недавно с нуля реализовал SheetHub (apiserver + web-UI + CLI + тесты), прошёл трёхлинзовое ревью с парой security-замечаний, починил всё за пять часов — и это смёржено. Ровно то, ради чего затевался open source.

Комьюнити — международное и англоязычное (но пока там один я), так что подключайтесь откуда угодно.

Итого

Sheeternetes — это, конечно, доведённый до абсурда мысленный эксперимент: «а что если единственным ограничением будет — держать всё внутри таблиц». Но по дороге он оказался настоящим: реальные контейнеры (в двух форматах — обычном OCI и нативном табличном), планировщик с bin-packing и failover, on-prem без интернета, федерация с живой миграцией и HMAC, стандарт SCRI/SICF, тесты, CI и даже фонд с сертификацией.

Ничего из этого не нужно в проде. Но, кажется, именно поэтому этим так весело заниматься — и, что неожиданно, на этом отлично объясняются вполне серьёзные вещи: что такое control plane, чем reconcile отличается от императивного деплоя, как устроены CRI и OCI, зачем нужен make-before-break и почему у образов есть дайджесты.

И помните: It reconciles.