Assnetes (1Ж: Оркестратор 8), или как мы заставили Kubernetes подчиниться правилам Тамары Петровны из бухгалтерии

Это неочевидно, но многие люди каждый день пишут код на языке 1С, проводят документы, закрывают месяцы и знают наизусть, что такое «Данные были изменены или удалены другим пользователем (сеансом)!». Kubernetes для большинства из них — что-то далёкое и чужое, с английскими словами, YAML-файлами и непонятной философией. Вчера в одном чатике по K8s Рамилька набросил про Kubernetes на 1С. Вызов был принят. Итак, что будет, если взять Kubernetes и перенести его в мир 1С, вместе с его языком, документами, регистрами и бухгалтерией за стенкой?
Получился дистрибутив, который я назвал Assnetes (или 1Ж:Оркестратор 8). Манифесты и операторы в нём пишутся на языке 1С (кириллицей), командная строка разговаривает голосом главного бухгалтера Тамары Петровны, а пакетный менеджер закупает в кластер Бухгалтерию и Битрикс вместе с операторами, один из которых умеет масштабировать кластер серверов 1С по количеству сеансов. Под всем этим работает самый обычный Kubernetes, и по дороге выяснилось, где он готов притвориться бухгалтерией, а где сопротивляется насмерть (могу его понять).
Исходники лежат в github.com/tym83/assnetes под лицензией Apache 2.0.
Зачем это всё
Это не первый мой эксперимент такого рода. Раньше я уже переносил управляющий слой Kubernetes в Google Таблицы (Sheeternetes), делал татароязычный форк вместе с документацией и консолью (Tatarnetes) и собирал дистрибутив, у которого управляющий слой устроен как мозаика из двух аллелей (Kyvernetria). Со стороны всё это похоже на шутки, но меня каждый раз интересовало одно и то же, насколько Kubernetes пластичен. Где у него жёсткое ядро, которое нельзя трогать, а где только наши привычки, которые можно заменить чем угодно, и система этого даже не заметит.
В этот раз вопрос звучал так. Можно ли сделать Kubernetes родным для целой профессии? Не локализовать интерфейс, а сделать так, чтобы человек, двадцать лет проработавший в 1С, открыл кластер и увидел в нём всё родное и привычное, то есть объекты, которые проводятся, справочники, подразделения, пометку удаления, регистры накопления и оборотно-сальдовую ведомость. Причём чтобы он узнал не только названия, но и встретил знакомое поведение, например, чтобы пометка удаления работала как в 1С, а не как синоним команды delete.
Подозрение у меня было такое, что модель Kubernetes и модель учётной системы гораздо ближе друг к другу, чем кажется. В Kubernetes вы описываете, каким должен быть объект, и отдаёте описание в систему, а контроллеры дальше приводят мир в соответствие с ним. В 1С вы заполняете документ и проводите его, а проведение формирует движения по регистрам. И там и там человек записывает, чего он хочет, а работу за него потом делает система, и мне хотелось посмотреть, как далеко можно протащить эту аналогию, прежде чем она начнет трещать по швам.
Сделано всё было за одно воскресенье на тестовом кластере в Cozystack на серверах Hetzner, и по дороге я не раз упирался в места, которые Kubernetes менять не даёт. О них тоже расскажу.
Как это выглядит
Начнём с простого. Вот обычное развёртывание nginx, которое в Kubernetes пишут на YAML, только записанное на языке 1С.
#Область Витрина
Развёртывание = Новый Развёртывание("Витрина", "Ромашка");
Развёртывание.Метаданные.Метки.Вставить("приложение", "витрина");
Развёртывание.Спецификация.Реплики = 2;
Развёртывание.Спецификация.Отбор.ПоМеткам.Вставить("приложение", "витрина");
Развёртывание.Спецификация.Шаблон.Метаданные.Метки.Вставить("приложение", "витрина");
Контейнер = Развёртывание.Спецификация.Шаблон.Спецификация.Контейнеры.Добавить();
Контейнер.Имя = "веб";
Контейнер.Образ = "nginx:1.29-alpine";
Контейнер.Ресурсы.Запросы.Процессор = "50m";
Провести(Развёртывание);
#КонецОбласти
Это не шаблон и не YAML, переодетый в кириллицу, а настоящая программа на языке 1С, которую выполняет интерпретатор. В ней можно завести переменные, написать цикл, вынести повторяющиеся фрагменты в процедуру или вычислить число реплик из параметра, и всё, что программа передаст в Провести, уйдёт в кластер. Отправляет её туда 1сctl, местный kubectl, и отвечает при этом примерно так, как ответила бы бухгалтерия.
$ 1сctl провести --файл витрина.bsl
✓ Развёртывание «Витрина» проведено
— Провела «Витрина». В следующий раз — со служебкой. (Тамара Петровна)
$ 1сctl получить поды --подразделение Ромашка
ИМЯ ГОТОВО ФАЗА ПЕРЕЗАПУСКИ СОСТОЯНИЕ ВОЗРАСТ
vitrina-74849bf66d-7xft9 1/1 Работает 0 ✓ проведён 23 сек
Обратите внимание на средний род. Развёртывание у 1сctl проведено, услуга (так здесь называется Service) проведена, под проведён, потому что он согласует причастие с родом вида объекта, а Тамара Петровна не простила бы ему «развёртывание проведён». Имя пода в таблице осталось латинским, и это первое место, где мы упираемся в особенности Kubernetes, о чём чуть ниже.
Команды у 1сctl тоже свои. Объекты проводятся, получаются, описываются и выгружаются обратно в исходник на языке 1С. Удалить объект можно и сразу, командой удалить, но по-1С-ному всё начинается с пометки на удаление, а само удаление выполняет регламентное задание, и только если на объект никто не ссылается. Журнал пода называется журналом регистрации. Пакеты закупаются, списываются и сторнируются, репозитории пакетов называются поставщиками, а их общий каталог — прайс-листом.
Словарь
Самым интересным в начале оказалось придумать словарь. Перевести Deployment как «Развёртывание» несложно, но это просто перевод. Хотелось найти для объектов Kubernetes такие понятия из мира учёта и кадров, которые не только звучат похоже, но и ведут себя похоже, и во многих случаях это получилось.
Kubernetes | 1Ж:Оркестратор 8 | Почему так |
|---|---|---|
Namespace | Подразделение | у каждого подразделения свои сотрудники, свои лимиты и свои права |
ServiceAccount | ПодотчётноеЛицо | тот, кому выдали доступ к ресурсам и с кого потом спросят |
ConfigMap | Справочник | данные, которые все читают и редко меняют |
Secret | Секрет, он же БланкСтрогойОтчётности | каждый экземпляр на учёте, и выносить нельзя |
HorizontalPodAutoscaler | СезонныйНайм | в пик нанимаем, после пика отпускаем |
PodDisruptionBudget | ГрафикОтпусков | сколько сотрудников можно одновременно отпустить, не остановив работу |
ResourceQuota | Лимит | лимит по статье расходов на квартал |
LimitRange | НормыРасхода | нормы списания на одну единицу |
Ingress | Проходная | через неё снаружи попадают внутрь |
Node | Узел, он же Склад | на нём лежат ресурсы, которые расходуют поды |
CustomResourceDefinition | РасширениеКонфигурации | ровно то, чем в 1С дорабатывают типовую конфигурацию |
kubeconfig | список информационных баз | каждый кластер в нём — отдельная база в окне запуска |
Есть и обратные случаи, когда понятие из 1С ложится на Kubernetes так хорошо, что его даже не пришлось переименовывать. Пометка удаления, после которой помеченное удаляется только тогда, когда на него никто не ссылается, очень похожа на финализаторы, которыми Kubernetes задерживает удаление объекта, пока за ним не приберут. Ближе всего к ней защита тома, которым ещё пользуется под, от удаления. Только пометку в 1С можно снять, а начатое удаление в Kubernetes отменить уже нельзя, поэтому пометка у нас сделана отдельно, а финализаторы остались за кадром. Проведение документа, после которого формируются движения по регистрам, устроено так же, как применение манифеста, после которого за дело берётся контроллер. А вкладка «Движения документа» на форме объекта в окне 1Ж показывает, какие объекты он породил, то есть то, что в Kubernetes записано в ссылках на владельцев.
Кириллица до самого API
Мне хотелось, чтобы человек вообще не видел латиницы, ни в манифестах, ни в выводе, ни в окне. Тут Kubernetes и показал, где у него жёсткое ядро.
Выяснилось, что Kubernetes спокойно принимает кириллицу в именах полей собственных видов объектов, тех, которые описываются через CRD. Схема может содержать поле Спецификация.КластерСерверов, API-сервер его сохранит, проверит и отдаст обратно, и контроллеру на Go с ним тоже можно работать. Поэтому все виды самого дистрибутива, вроде информационной базы, кластера серверов 1С или производственного календаря, хранят свои поля в API прямо по-русски, без всякого перевода.
А вот имена видов (то, что в Kubernetes называется kind), имена объектов и группы API Kubernetes принимает только латиницей, потому что это имена из мира DNS, а у меток правила хоть и свои, но такие же строгие. С группой API получилось смешнее всего. Хотелось, чтобы виды дистрибутива жили в группе оркестратор.бух, и раз это имя из мира DNS, то и записать его можно так, как в DNS записывают кириллические домены, то есть через punycode. Так что в API группа называется xn--80ajopdiccfkc.xn--90a0bg, а человек видит её как оркестратор.бух. Пакеты по той же логике живут в группе пакеты.бух.
Для всего остального есть переводчик, который работает на пути в API и обратно. Виды и поля стандартных объектов он переводит по словарю, так что Развёртывание становится Deployment, а Спецификация.Реплики — spec.replicas. Имена объектов транслитерируются почти по ГОСТу, «Витрина» превращается в vitrina, а оригинал сохраняется в аннотации на самом объекте. Когда объект возвращается из кластера, переводчик находит эту аннотацию и восстанавливает имя, поэтому 1сctl выгрузить развёртывание витрина отдаёт тот же манифест на языке 1С, который вы когда-то провели. С метками то же самое, только их русские оригиналы хранятся все вместе в одной аннотации, чтобы не заводить по аннотации на каждую.
Поды создаются из развёртывания через промежуточный объект, и имена им Kubernetes строит сам, из имени развёртывания и случайного хвоста. Про нашу аннотацию он ничего не знает, поэтому имена подов остаются транслитом, как vitrina-74849bf66d-7xft9 в примере выше. Это та граница, дальше которой без переписывания самого Kubernetes не пройти, и я решил, что пусть она будет видна.
Несколько мелочей, на которых переводчик спотыкался
В схемах CRD можно описывать проверки полей на языке выражений CEL, но обратиться из такого выражения можно только к полю с латинским именем. Поля с кириллицей для CEL просто не существуют, поэтому проверки, в которых участвует больше одного поля, пришлось делать вебхуками, то есть отдельной программой, к которой API-сервер обращается перед записью объекта.
Ключи в справочнике (ConfigMap) тоже должны быть латинскими, поэтому ключ настройки.ini уходит в кластер как nastrojki.ini, а оригинал запоминается рядом. При этом имена переменных окружения трогать нельзя ни в коем случае, а на первом прогоне MYSQL_ROOT_PASSWORD превратился в mysql-root-password, после чего MySQL отказался запускаться без пароля. Теперь вложенные имена переводятся, только если в них есть кириллица.
Слово, которым человек называет вид, приходит в разных падежах. В «получить услугу» вид стоит в винительном, и словарь его не находил, пока я не научил поиск отбрасывать окончания и сравнивать основы.
Финализаторы и подразделения в выводе поначалу показывались транслитом и punycode, хотя всё остальное возвращалось по-русски. Финализаторы теперь переводятся так же, как ключи аннотаций, а настоящее имя подразделения 1сctl берёт из аннотации на самом Namespace, один раз за запуск.
Свой интерпретатор языка 1С
Открытый интерпретатор языка 1С уже существует, это OneScript, но он живёт в .NET, а мне нужно было встроить язык в программу на Go, чтобы на нём работали и 1сctl, и операторы в кластере. Поэтому интерпретатор пришлось написать свой. Он понимает не весь встроенный язык, но зато знает всё, что нужно для манифестов и операторов: процедуры и функции, исключения, структуры и соответствия, таблицы значений и большинство привычных функций вроде СтрШаблон и Формат. Ключевые слова работают и по-русски, и по-английски, как в настоящей платформе.
Отдельно пришлось позаботиться о числах. В 1С числа десятичные, и бухгалтер, который сложит 0,1 и 0,2, получит ровно 0,3, а не 0,30000000000000004. Мой интерпретатор хранит числа как точные дроби, поэтому это выражение у него тоже истинно, а Формат(1234567.891, "ЧДЦ=2") возвращает 1 234 567,89 с неразрывным пробелом между разрядами, как и положено. Есть, правда, одно расхождение, которое я честно оставил. Треть, умноженная на три, у него ровно единица, а в платформе получится 0,999… с длинным хвостом девяток.
$ cat проверка.os
Сообщить(0.1 + 0.2 = 0.3);
Сообщить(Формат(1234567.891, "ЧДЦ=2"));
$ 1сctl выполнить проверка.os
Да
1 234 567,89
Операторы на языке 1С
Самое интересное начинается, когда на языке 1С пишутся не только манифесты, но и операторы, то есть программы, которые живут в кластере и приводят мир в соответствие с объектами. В Kubernetes оператор обычно пишут на Go, и в нём много обвязки: подписки на события, очереди, повторы, финализаторы, статусы, вебхуки, выбор лидера. Мне хотелось, чтобы человек, который пишет на 1С, видел из всего этого только то, к чему привык, то есть обработчики событий.
Оператор здесь называется обработкой. Это модуль на языке 1С, а если у обработки есть свои виды объектов, то ещё и их описание в метаданных. Обработчики событий помечаются аннотациями и называются так же, как события модуля объекта, а устроены они ближе всего к подпискам на события, то есть это экспортные процедуры, которым первым параметром приходит сам объект.
&Обработка(Вид = "ИнформационнаяБаза")
Процедура ОбработкаПроведения(Объект, Отказ) Экспорт
// привести мир к объекту
КонецПроцедуры
&ПередУдалением(Вид = "ИнформационнаяБаза")
Процедура ПередУдалением(Объект, Отказ) Экспорт
// прибрать за объектом, пока он не исчез
КонецПроцедуры
&ПроверкаЗаполнения(Виды = "Развёртывание", Операции = "Создание, Изменение")
Процедура ОбработкаПроверкиЗаполнения(Объект, Отказ, Контекст) Экспорт
// не пустить в кластер то, чему там не место
КонецПроцедуры
&Расписание("17 * * * *")
Процедура УдалитьПомеченные() Экспорт
// регламентное задание
КонецПроцедуры
Всё остальное делает среда исполнения обработок. ОбработкаПроведения вызывается, когда объект создан или изменён, а ещё при запуске и время от времени просто для сверки, поэтому повторный вызов не должен ничего ломать. Если процедура выставила Отказ, то в 1С документ просто остался бы непроведённым, а здесь объект встаёт в очередь и через минуту будет проведён снова. ПередУдалением работает под финализатором, и пока он не отработал без отказа, объект не исчезнет. ОбработкаПроверкиЗаполнения превращается в проверяющий вебхук (validating admission webhook) со своим сертификатом, и отказ в нём означает, что API-сервер не примет объект. Если же сама среда исполнения лежит, кластер не блокируется, и проверка молча пропускается, так что запрет здесь скорее строгий, чем абсолютный. Состояние, события, повторы, выбор лидера и прочее среда берёт на себя. Кластер модуль видит через глобальный объект Кластер, у которого есть Выбрать, Получить, Провести и Удалить, а ещё в модуле есть HTTPСоединение с проверкой сертификатов, чтение XML и JSON и всё остальное, что нужно, чтобы сходить во внешний мир.
С дистрибутивом идут пять операторов, и каждый из них делает что-то полезное, а не только just for fun.
Производственный календарь загружает с xmlcalendar.ru производственный календарь на год и не даёт выкатывать что-либо в праздники, в выходные и в предпраздничный сокращённый день после 15:00. Переносы выходных и сокращённые дни он знает. Если выкатить всё-таки очень надо, на объект кладётся служебная записка с обоснованием, и календарь пропускает его, записав в журнал, кто и почему это сделал.
Курсы валют каждый час считают, во сколько каждое подразделение обходится в месяц по запросам ресурсов в валютах, которые вы укажете.
Дата запрета изменения закрывает период, как одноимённая настройка в 1С. Объекты подразделения, созданные до этой даты, после неё могут менять и удалять только главный бухгалтер и администраторы кластера, а всем остальным достаётся «Период закрыт… обратитесь к главному бухгалтеру». Датой документа здесь служит дата создания объекта, потому что другой даты у объектов Kubernetes нет.
Удаление помеченных раз в час обходит помеченные на удаление объекты и удаляет только те, на которые никто не ссылается. Если ссылки есть, объект остаётся, а в его событиях появляется список тех, кто на него ссылается. По-моему, это честнее, чем поведение Kubernetes, который без возражений даст удалить справочник, на который ссылаются три развёртывания, и вы узнаете об этом, только когда очередной под не сможет стартовать.
Акт сверки сверяет кластер с манифестами в Git, как с контрагентом, и составляет акт сверки с таблицей расхождений. Если расхождений нет, акт готов к подписанию, а если есть, то сальдо, понятное дело, в пользу контрагента.
Вот так выглядит кусок производственного календаря, тот, где решается, пускать ли выкатку.
&ПроверкаЗаполнения(Виды = "Развёртывание, НаборСоСостоянием, НаборНаКаждомУзле, РегламентноеЗадание, Сайт", Операции = "Создание, Изменение")
Процедура ОбработкаПроверкиЗаполнения(Объект, Отказ, Контекст) Экспорт
Если Объект.Метаданные.Владельцы.Количество() > 0 Тогда
Возврат; // объект ведёт оператор: проверяли того, кто им владеет
КонецЕсли;
Если Контекст.Операция = "Изменение"
И ДанныеСовпадают(Объект.Спецификация, Контекст.СтарыйОбъект.Спецификация) Тогда
Возврат; // спецификация та же: это не выкатка
КонецЕсли;
Календари = Кластер.Выбрать("ПроизводственныйКалендарь");
Для Каждого Календарь Из Календари Цикл
// ... выбрать календарь на текущий год и найти причину запрета ...
Записка = Объект.Метаданные.Аннотации.Получить("оркестратор.бух/служебная-записка");
// ... а если на объекте записки нет, поискать её на подразделении ...
Если ЗначениеЗаполнено(Записка) Тогда
Журнал.Информация(СтрШаблон("%1, но есть служебная записка от %2: «%3». Пропускаю.",
Причина, Контекст.Пользователь, Записка));
Возврат;
КонецЕсли;
Отказ = Истина;
Сообщить(СтрШаблон("%1. Выкатки запрещены производственным календарём. Если очень надо — ...", Причина));
Возврат;
КонецЦикла;
КонецПроцедуры
А так выглядит попытка выкатить что-нибудь в воскресенье, 25 октября, когда я эту статью и писал.
$ 1сctl провести --файл касса.bsl
✗ Отказ в проведении. Сегодня 25 октября, выходной. Выкатки запрещены производственным календарём. Если очень надо — служебная записка: аннотация «оркестратор.бух/служебная-записка» с обоснованием на объекте или на его подразделении.
Ошибка: не проведено документов: 1 из 1
Тут случилась история, которая мне нравится больше всего во всём проекте. Календарь оказался настолько работоспособным, что в воскресенье (то есть когда я и собирал проект) заблокировал установку оператора PostgreSQL, которую делал пакетный менеджер, потому что тот тоже выкатывает развёртывания, а в выходные так нельзя. Прикладывать записку к каждому своему объекту пакетный менеджер не умеет, поэтому календарь научился читать служебную записку ещё и на подразделении целиком. А в журнале календаря теперь можно прочитать, кто и под каким предлогом выкатывал в выходной.
Сегодня 25 октября, выходной, но есть служебная записка от kubernetes-super-admin: «Клиент ждёт третью реплику до понедельника. Согласовано с директором устно.». Пропускаю.
Сегодня 25 октября, выходной, но есть служебная записка от lenochka: «срочно». Пропускаю.
Голос бухгалтерии
Раз уж календарь ведёт себя как живой человек, пора познакомиться с остальными сотрудниками. У дистрибутива есть несколько персонажей. Главный бухгалтер Тамара Петровна проводит документы, ворчит и требует первичку. Леночка ничего не нажимала, оно само. Валентина Ивановна с 12:00 до 13:00 на обеде, и окно 1Ж весь обед показывает плашку «У меня обед. Касса откроется в 13:00», хотя работать не мешает. А в углу окна есть лента «Девочки из бухгалтерии», где Ира обиделась на Cilium, хотя не знает, кто это, а Света спрашивает, заразен ли CrashLoopBackOff.
Персонажи знают бухгалтерский календарь. Двадцать пятого Тамара Петровна сдаёт отчётность и просит всё, что не горит, нести после двадцать восьмого, когда она заплатит единый налоговый платёж. В первые рабочие дни месяца она закрывает месяц и ничего не проводит задним числом, зато в День бухгалтера, двадцать первого ноября, проведёт и без служебки. Все фразы собраны в одном файле, и чтобы добавить свою, достаточно прислать пул-реквест, код трогать не нужно.

Ошибки API тоже переведены, причём не на русский язык, а на язык платформы 1С, который её пользователи узнают с полуслова. Конфликт версий при записи объекта выглядит как «Данные были изменены или удалены другим пользователем (сеансом)!», превышение квоты звучит как «Нет свободной лицензии. Лимит по статье исчерпан — приходите в следующем квартале», а API-сервер, который ответил, что недоступен, превращается в «Ошибка соединения с сервером 1С:Предприятия 8.3: Сервер 1С:Предприятия не обнаружен. Позвоните 1С-нику».
1С:Предприятие в кластере
Теперь о той части, которая может пригодиться и без всякой пародии. Готового оператора, который ведёт кластер серверов 1С в Kubernetes, насколько я знаю, нет, а желание держать 1С рядом с остальной инфраструктурой у тех, кто строит частные облака, встречается. Поэтому оператор 1С:Предприятия здесь получился настоящим. Он сделан по образцу операторов баз данных вроде CloudNativePG, которые сами создают базу, следят за ней, обновляют и масштабируют, так что администратор только пишет, чего хочет. И, разумеется, написан он на языке 1С.
Сразу оговорюсь, что платформу 1С здесь изображает заглушка, о ней в конце раздела, поэтому и поставка в примере называется соответствующе. Вот как выглядит Бухгалтерия в кластере (саму поставку я опустил).
КС = Новый КластерСерверов("Основной", "Бухгалтерия");
КС.Спецификация.Поставка = "8.3.27-заглушка";
КС.Спецификация.РабочиеСерверы.Количество = 1;
КС.Спецификация.РабочиеСерверы.Память = "512Mi";
КС.Спецификация.Масштабирование.Включено = Истина;
КС.Спецификация.Масштабирование.СеансовНаСервер = 2;
КС.Спецификация.Масштабирование.Максимум = 3;
КС.Спецификация.СУБД.Объём = "2Gi";
Провести(КС);
ИБ = Новый ИнформационнаяБаза("Бухгалтерия", "Бухгалтерия");
ИБ.Спецификация.КластерСерверов = "Основной";
ИБ.Спецификация.Шаблон.ШаблонКонфигурации = "БухгалтерияПредприятия-3.0.202.14";
Провести(ИБ);
Публикация = Новый Публикация("Бухгалтерия", "Бухгалтерия");
Публикация.Спецификация.ИнформационнаяБаза = "Бухгалтерия";
Публикация.Спецификация.Адрес = "/buh";
Провести(Публикация);
Документ поставки описывает дистрибутив платформы, кластер серверов описывает центральный и рабочие серверы, а под базу данных оператор заказывает кластер PostgreSQL у CloudNativePG. Когда под рабочего сервера готов, оператор регистрирует его в кластере 1С через rac, как это сделал бы администратор. Информационную базу он создаёт через ibcmd в PostgreSQL из шаблона конфигурации и регистрирует в кластере, по документу «Публикация» публикует её на веб-сервере, а при удалении снимает базу с учёта в кластере и сохраняет данные, если его не попросили об обратном.
Масштабирование я сделал так, как его понимают в 1С. Kubernetes привык масштабировать по процессору и памяти, но рабочий сервер 1С живёт сеансами, поэтому оператор считает сеансы и держит столько рабочих серверов, чтобы на каждый приходилось не больше заданного числа. При программной лицензии он не выходит и за число купленных серверных лицензий, ведь каждому новому сотруднику нужно своё рабочее место, и если их не хватает, честно пишет в журнал, сколько нужно и сколько куплено. А вот увольнять сервер сразу нельзя: на нём сидят люди. Поэтому сначала оператор запрещает ему новые сеансы правилом «Не назначать» и применяет его частично, чтобы не трогать тех, кто уже работает, потом ждёт, пока текущие сеансы закончатся, и только потом исключает сервер из кластера. В журнале это выглядит так.
Кластер «osnovnoj»: принят на работу сервер osnovnoj-rabochij-1.osnovnoj-servery
Кластер «osnovnoj»: серверу osnovnoj-rabochij-1.osnovnoj-servery выставлен безопасный объём памяти рабочих процессов 358 МБ
Кластер «osnovnoj»: сервер osnovnoj-rabochij-1.osnovnoj-servery больше не принимает сеансы — дорабатывает
Кластер «osnovnoj»: сервер osnovnoj-rabochij-1.osnovnoj-servery уволен по истечении срочного трудового договора
Вертикальное масштабирование тоже есть, и в оригинале оно называется повышением оклада по служебной записке. Когда меняется память рабочих серверов, оператор пересчитывает для них безопасный и критический объёмы памяти рабочих процессов и выставляет их через rac. Так кластер 1С сам заменяет разросшийся рабочий процесс новым, а при критическом объёме прерывает самый прожорливый серверный вызов, и всё это раньше, чем контейнер упрётся в лимит памяти и ядро убьёт его целиком вместе со всеми сеансами.
Обновление конфигурации оформляется отдельным документом, и оператор проводит его так, как это делал бы аккуратный администратор. Он запрещает начало сеансов и регламентные задания с кодом разрешения, завершает тех, кто ещё сидит в базе, выгружает базу в файл на случай отката, загружает и применяет обновление и только потом снимает блокировку. Обработчики обновления самой конфигурации выполнятся, как обычно, при первом входе администратора.

Теперь о главном ограничении. Платформу 1С:Предприятие нельзя распространятьё и в публичный образ контейнера её не положишь, регистрироваться на портале разработчиков желания нет вообще:) Поэтому всё описанное я проверял на заглушке, которую написал по документации. Заглушка изображает служебные программы платформы, через которые администрируют кластер серверов и создают базы, отвечает в том формате, который описан в документации фирмы «1С», ведёт реестр кластера, серверов и сеансов, а информационные базы создаёт как настоящие базы данных в PostgreSQL. Оператор не знает, с чем именно он разговаривает, и с настоящей платформой должен работать так же, если в документе поставки указать свой архив платформы вместо заглушки.
Битрикс
Раз уж в кластере завелась Бухгалтерия, интернет-магазину тоже нашлось место. 1С-Битрикс получил своего оператора. Документ «Сайт» описывает редакцию, веб-узлы (сколько держать и до скольких растить в сезон) и базу данных, а оператор поднимает MySQL, Redis, веб-кластер, проходную и сезонный найм, переводит агенты Битрикса на cron ведущего узла и в итоге отдаёт мастер установки.
Сайт = Новый Сайт("Магазин", "Торговля");
Сайт.Спецификация.Редакция = "start";
Сайт.Спецификация.Веб.Реплики = 2;
Сайт.Спецификация.Веб.МаксимумРеплик = 4;
Сайт.Спецификация.БазаДанных.Объём = "5Gi";
Провести(Сайт);
Самым трудным здесь оказались файлы. Битрикс хранит сайт в файловой системе и хочет, чтобы все веб-узлы видели одни и те же файлы. Модуль «Веб-кластер», который умеет это из коробки, в редакции «Старт» не положен, а на тестовом кластере не было хранилища, которое можно подключить к нескольким подам сразу, и NFS на его узлах не монтировался. Поэтому веб-кластер пришлось собрать самим. Один узел ведущий, на нём лежат файлы, и туда же уходят запросы, которые их меняют, а остальные узлы держат зеркало и забирают изменения через rsync. Мастер установки на скриншоте как раз отдаётся с такого веб-кластера, а сам документ «Сайт» в это время стоит в фазе «ОжидаетМастерУстановки». Дальше нужно принять лицензионное соглашение Битрикса, и это уже решение человека, а не оператора.

Прайс-лист
Ставится всё это пакетным менеджером kubepkg, который я делаю отдельно. Внутри 1сctl он встроен целиком, только работает в группе пакеты.бух и разговаривает по-бухгалтерски.
$ 1сctl ввод-начальных-остатков
$ 1сctl закупить бухгалтерия
$ 1сctl остатки
НАИМЕНОВАНИЕ ВЕРСИЯ РЕДАКЦИЯ ГОТОВ СОСТОЯНИЕ
1c-bitrix 0.2.0-rc.2 3 да ✓ проведён
1c-buhgalteriya 0.2.0-rc.1 1 да ✓ проведён
1c-predpriyatie 0.2.0-rc.2 11 да ✓ проведён
assnetes-obrabotki 0.2.0-rc.2 3 да ✓ проведён
cloudnative-pg 1.30.1 4 да ✓ проведён
Команда ввод-начальных-остатков, названная так в честь того, с чего начинается учёт в любой новой базе, ставит в кластер пакетный менеджер и подписывает его на двух поставщиков. Первый — наш прайс-лист с продуктами 1Ж, второй — основной репозиторий kubepkg, откуда берётся CloudNativePG. В прайс-листе есть оператор 1С:Предприятия, 1С:Бухгалтерия, 1С:Управление торговлей, 1С:CRM, 1С-Битрикс и обработки. Продукты, построенные на платформе, приносят с собой шаблоны конфигураций для заглушки (настоящие файлы конфигураций приходят с подпиской на обновления 1С), поэтому вторую базу можно завести одной строкой, сославшись на шаблон из прайс-листа, как в примере выше с ШаблонКонфигурации.
Такие эксперименты полезны ещё и тем, что они нагружают инструменты непривычным образом. Пока я собирал прайс-лист, в kubepkg нашлись две настоящие ошибки. Первая ломала установку пакета, если его виды объектов уже были в кластере и их записал кто-то другой. Из-за второй пакет, поставленный из версии-кандидата, сам себе не подходил, потому что kubepkg записывал ограничение ~0.2, а по принятым правилам сравнения версий оно не пропускает версию-кандидат 0.2.0-rc.2, ту самую, которую только что поставили. Обе я исправил в самом kubepkg, так что пользоваться им станет удобнее всем, а не только бухгалтерии.
Запросы и оборотно-сальдовая ведомость
Когда Бухгалтерия закуплена, хочется понять, сколько она съела, и тут без языка запросов не обойтись. 1сctl запрос выполняет запрос на подмножестве языка запросов 1С к объектам кластера, с группировками, агрегатами, ВЫБОР КОГДА, ПОДОБНО, параметрами и упорядочиванием. Из модуля обработки им можно пользоваться через привычный Новый Запрос.
Запрос = Новый Запрос(
"ВЫБРАТЬ
| П.Подразделение КАК Подразделение,
| КОЛИЧЕСТВО(*) КАК Всего,
| СУММА(ВЫБОР КОГДА П.Фаза = ""Running"" ТОГДА 1 ИНАЧЕ 0 КОНЕЦ) КАК Работает
|ИЗ
| Кластер.Поды КАК П
|СГРУППИРОВАТЬ ПО
| П.Подразделение
|УПОРЯДОЧИТЬ ПО
| Всего УБЫВ");
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(СтрШаблон("%1: %2 из %3", Выборка.Подразделение, Выборка.Работает, Выборка.Всего));
КонецЦикла;
Есть и регистры накопления. РегистрНакопления.РесурсыУзлов.ОстаткиИОбороты показывает для каждого узла приход (сколько ресурсов узел может дать), расход (сколько у него запросили поды) и остаток. А РегистрНакопления.Перезапуски.Обороты показывает перезапуски контейнеров, что очень удобно, когда нужно найти, кто тут у нас падает чаще всех. Из регистра ресурсов строится оборотно-сальдовая ведомость, и узлы в ней, конечно, учитываются на счёте 01 как основные средства, с субконто «Узлы». Дебет — то, что узел даёт, кредит — то, что у него запросили поды, а сальдо — то, что осталось.
$ 1сctl осв
Оборотно-сальдовая ведомость по счёту 01 «Основные средства», субконто «Узлы»
Дебет — что даёт узел, кредит — что запросили поды. Ядра — в штуках, память — в ГиБ.
УЗЕЛ ЯДРА ОБОРОТ ДТ ЯДРА ОБОРОТ КТ ЯДРА САЛЬДО ДТ ПАМЯТЬ ОБОРОТ ДТ ПАМЯТЬ ОБОРОТ КТ ПАМЯТЬ САЛЬДО ДТ ПОДОВ
kubernetes-stand-md0-txgm9-2dj4m 3,6 1,27 2,33 12,9 2,52 10,38 14
kubernetes-stand-md0-txgm9-44kkb 3,6 1,43 2,17 12,9 2,7 10,2 13
kubernetes-stand-md0-txgm9-q759r 3,6 1,62 1,98 12,9 3,38 9,52 16
Окно 1Ж:Предприятие
Командная строка хороша, но 1С-ник живёт в окне, и 1С не была бы 1С без своего окна. 1сctl гуи поднимает на локальной машине веб-интерфейс, похожий на «Такси», к которому 1С-ники привыкли за последние десять с лишним лет, с разделами слева, панелью навигации и формами объектов во вкладках. Начинается всё с окна запуска. Список информационных баз, который платформа хранит в файле ibases.v8i, здесь берётся из вашего kubeconfig, и каждый кластер в нём выглядит как отдельная база, так что из одного окна можно управлять сразу несколькими.

Внутри есть монитор руководителя, списки объектов по разделам, формы объектов с исходником на языке 1С, состоянием и движениями документа, кнопки «Провести и закрыть» и «Пометить на удаление», консоль запросов и отчёты. Провести объект можно прямо из формы, а удалить, как и положено, нельзя, только пометить.

Узел, который сам закупает бухгалтерию
А чтобы не ставить всё это руками, я собрал 1Ж как отдельный дистрибутив на основе kuberoot, нашего Kubernetes со своим ядром и своей системой инициализации. За основу взят вариант, который превращает одну машину в целый кластер, а своё дополнение у дистрибутива одно, подписка узла на прайс-лист 1Ж. Написано оно, конечно, на языке 1С, и при сборке образа 1сctl переводит его в обычный манифест.
Прайс = Новый ОбъектКластера("Repository", "прайс-лист");
Прайс.ВерсияAPI = "kubepkg.dev/v1";
Прайс.Спецификация.url = "https://tym83.github.io/assnetes/index.yaml";
Прайс.Спецификация.interval = "10m";
Прайс.Спецификация.publicKeys = Новый Массив;
Прайс.Спецификация.publicKeys.Добавить(Параметры.Ключ);
Провести(Прайс);
Загруженный с такого образа узел поднимает Kubernetes, ставит пакетный менеджер и сам закупает всё, что есть в прайс-листе. На тестовой виртуалке от включения до работающих операторов 1С, Битрикса, обработок и PostgreSQL прошло около двенадцати минут. После этого на узле можно было завести кластер серверов, две информационные базы (одну из них по шаблону из прайс-листа) и публикацию, и всё это заработало без единой ручной команды на самом узле.
Что из этого следует
Главное, что я для себя понял, это то, что Kubernetes очень пластичен во всём, что касается данных, и очень упрям во всём, что касается имён. Поля объектов, язык манифестов и операторов, тексты ответов и ошибок можно заменить, и ни API-сервер, ни контроллеры этого не заметят. А имена видов, объектов, меток и групп привязаны к DNS и близким к нему правилам, и обойти это можно только переводчиком на входе и выходе, с аннотациями, в которых хранится то, что человек написал на самом деле. Поэтому латиница всё равно проступает там, куда переводчик не дотягивается, например в именах подов, которые Kubernetes придумывает сам.
Аналогия, с которой всё началось, выдержала больше, чем я ожидал. Трескалась она в предсказуемых местах, на именах, на внешнем мире вроде лицензионного соглашения Битрикса, которое должен принимать человек, и на мелочах, которые 1С-ник заметит сразу, вроде того, что пометку удаления можно снять, а удаление в Kubernetes нельзя отменить. Но в главном учётная система и система оркестрации действительно решают одну и ту же задачу, только одна учитывает деньги и товары, а другая контейнеры и ядра. И ответ на вопрос из начала, кажется, такой. Для 1С-ника барьер перед Kubernetes стоит в словах, а не в модели, и модель ему ближе, чем принято думать.
А ещё у этой истории есть практическая сторона. Оператор кластера серверов 1С, который масштабирует его по сеансам и аккуратно обновляет конфигурацию, может пригодиться и без всякой пародии, особенно тем, кто переносит 1С в частное облако вместе с остальной инфраструктурой. Для этого его нужно проверить на настоящей платформе. Если у вас есть платформа, лицензии и желание погонять оператор на настоящем кластере серверов, напишите мне, я буду очень рад.
Как попробовать
Понадобится любой Kubernetes и kubeconfig к нему. Бинарники 1сctl для Linux, macOS и Windows выложены в релизах. Имена у них латинские, потому что GitHub портит кириллицу в именах файлов, так что после скачивания нужный файл стоит переименовать в 1сctl.
1сctl ввод-начальных-остатков # пакетный менеджер и прайс-лист
1сctl прайс # что можно закупить
1сctl закупить бухгалтерия # оператор 1С, PostgreSQL и 1С:Бухгалтерия
1сctl провести --файл examples/buhgalteriya-1c.bsl
1сctl гуи # окно на http://127.0.0.1:8383/
Примеры манифестов собраны в каталоге examples репозитория, а обработки, с которых удобно начинать писать свои, — в obrabotki. Если найдёте ошибку или захотите добавить фразу Тамаре Петровне, приходите в репозиторий.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.