Оффбординг на автомате: почему «уволен» — слишком поздний сигнал, а «удалить» — шаг, который уже не отыграть

от автора

Четыре узла, три стрелки. Согласование. Резервная копия данных уходящего. Вебхук в свою систему — скажем, поставить задачу на передачу дел. Удаление пользователя.

Такой сценарий увольнения собирается в любом визуальном конструкторе минут за пять, и придраться вроде не к чему. Порядок правильный: данные сохраняются раньше, чем исчезают.

Меня в этой картинке интересует ровно одно место. Что будет, если она рассыплется между вторым узлом и четвёртым.

Я руковожу компанией +Альянс и по работе занимаюсь автоматизацией рутинных операций в облачных офисах. Названий инструментов ниже не будет ни одного, своего в том числе: разговор о классе движков и о том, чего от них требовать. Яндекс 360 называть придётся, но это платформа, а не мой продукт.

Онбординг и оффбординг пишут через слэш, а движку это разные задачи

Их всегда упоминают парой, будто одна задача в две стороны. Исполнителю — нет.

Онбординг прекрасно вешается на событие. В Яндекс 360 появилась учётная запись — автоматика подхватила и пошла: дозаполнила профиль, перевела человека в отдел, добавила в рабочие группы, отправила приветственное письмо. То, что событие приходит по факту случившегося, а не заранее, здесь никому не мешает. Запись уже создана, с ней и работаем.

И главное — в онбординге всё обратимо. Не тот отдел? Переведёте завтра. Письмо ушло дважды? Человек поморщится и забудет. Цена ошибки измеряется в неловкости.

Оффбординг похож только снаружи. У него два неприятных свойства: упирается в действие, которое нельзя отыграть, и плохо с точкой старта. Начну со старта, он проще.

Событие приходит, когда спасать уже нечего

В Яндекс 360 событие увольнения означает удаление учётной записи. Не “приказ подписан”, не “последний рабочий день” — именно удаление.

Дальше арифметика. Автоматика узнаёт о событии постфактум, когда учётной записи уже нет, и снимать полную копию в этот момент попросту не с чего.

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

Забавно получается: автоматизация оффбординга начинается с решения не автоматизировать его старт. Кнопку жмёт человек. А вот всё, что после кнопки, от человеческой памяти зависеть уже не должно.

Кстати, неплохой тест для чужого решения. Показывают готовый сценарий “по событию увольнения снимаем копию и удаляем пользователя” — спросите, в какой момент прилетает это событие. Дальше можно не смотреть.

Стрелка на холсте ничего не гарантирует

Порядок “сначала копия, потом удаление” кажется вопросом рисования. Он не вопрос рисования.

Стрелка говорит “иди сюда после”. Что считать словом “после” — она не сообщает.

Копирование почтового ящика — операция долгая и асинхронная по природе. Узел вполне может получить от системы копирования ответ “принято в работу” и с чистой совестью передать управление дальше. Граф красивый, стрелки на местах, а удаление прилетает в момент, когда копия снята наполовину. Худший из возможных исходов: формально всё отработало, статус зелёный, данных нет.

Первое, что закрывает эту дыру, — гейтинг необратимых действий. Удаление сотрудника выполняется только после успешного завершения резервного копирования: узел копии держит выполнение, пока она не закончена. Не “запустил и побежал дальше”, а “жду”.

Второй ход мне нравится больше, потому что он структурный, а не поведенческий. Пусть узел удаления берёт идентификатор пользователя не из триггера и не из вбитого руками поля, а из результата узла копирования — подстановкой вида {backup.uid}. Тогда “сначала копия” перестаёт быть договорённостью администратора с собственной совестью. Без результата предыдущего шага удалению нечего подставить в вызов. Физически нечего.

Приём копеечный. Работает лучше любого регламента.

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

Что происходит, когда узел падает

Вот здесь и проходит граница между конструктором картинок и движком.

Временные ошибки — таймауты, недоступность API, пятисотки — лечатся повторами с экспоненциально растущей паузой. Когда попытки исчерпаны, запуск честно завершается ошибкой, и текст ошибки сохраняется, а не растворяется в логах. Это минимум, ниже которого разговаривать не о чем.

Дальше приходит человек и жмёт “перезапустить”. И тут выясняется, кто как понял это слово.

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

Но и оно закрывает не всё. Остаётся класс ситуаций, когда узел исполняется повторно сам по себе: повторная доставка задачи, гонка, ретрай на транспортном уровне. Поэтому отдельным свойством идёт идемпотентность необратимых действий — при повторном исполнении узла результат ранее выполненного необратимого шага переиспользуется, а не выполняется заново.

Для узла “отправить письмо” это вопрос вежливости. Для узла “удалить пользователя” — вопрос о том, что вернёт API на второе удаление уже удалённого и как на это отреагирует граф. Проверять такое на живом тенанте я никому не советую. Пусть второй проход будет no-op с прежним результатом, и тема закрыта.

Согласование — это состояние процесса, а не письмо

В оффбординге согласование обычно принимают за формальность. Письмо руководителю, галочка, поехали дальше.

Инженерно это другая сущность. Блокирующее состояние: пока согласующие не ответили, выполнение стоит. Копирование не начнётся, а узел удаления так и останется недостижимым. Кворум при этом должен настраиваться — иначе процедура либо намертво встаёт из-за одного человека в отпуске, либо проходит по решению первого откликнувшегося.

А теперь то, ради чего раздел написан.

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

И вторая сторона того же письма, про которую спрашивают реже. Чтобы его отправить, движок держит у себя рабочий почтовый секрет — то есть возможность писать от имени вашего служебного ящика. Так что вопрос не только про ссылку, но и про то, как хранятся секреты узлов и что отдаётся на чтение. Нормальный ответ — зашифрованы в базе, наружу уходит маска вместо значения. Ненормальный — “ну, они же у нас внутри”.

Блокировка и отзыв доступа — разные операции

Пропускают это регулярно.

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

Поэтому рядом с блокировкой в графе обязан стоять отдельный узел отзыва сессий и токенов. Не опция в свойствах, а соседний шаг. “Мы же заблокировали” и “доступа нет” — разные утверждения, и расстояние между ними измеряется не секундами.

Ещё несколько мест, которые выясняются потом

Граф стоит проверять до включения, а не в момент исполнения. Циклы, недостижимые узлы, битые связи — всё это ловится статически, и невалидный сценарий не должен стартовать вообще. Без такой проверки вы узнаёте о недостижимом узле в тот день, когда им оказался узел отзыва сессий. И узнаёте не сами. Рядом живёт потолок одновременных запусков на организацию — настройка скучная ровно до первой реорганизации отдела, когда пачка событий поднимает пачку запусков и все они синхронно идут в один облачный API.

Отдельная история — узел исходящего вебхука. Штука, которая по вашей команде ходит на произвольный URL, живёт внутри вашего контура и подставляет в тело запроса данные из сценария. То есть у неё есть и сетевая позиция, и параметризация. Дальше дело техники: внутренние диапазоны, служебные адреса инфраструктуры, локальные админки, до которых снаружи не достучаться. Отклонять такие адреса обязан сам узел — не инструкция для того, кто его настраивает, и не бдительность автора сценария. Иначе вы своими руками собрали сканер периметра с удобным веб-интерфейсом.

И журнал запусков: не список статусов, а подсветка пройденного пути — какой узел отработал, где встало, с каким текстом ошибки. Через год кто-нибудь придёт с конкретной фамилией и спросит, чем закончилась процедура для этого человека. Хорошо, когда ответ можно открыть, а не вспомнить.

Неприятное свойство надёжного автомата

У всей конструкции есть побочный эффект, и прятать его я не собираюсь. Чем надёжнее движок, тем меньше в процедуре остаётся человека. Кнопку в начале нажимает оператор, а дальше решает граф — и решает быстро. Если в графе ошибка, он отработает её аккуратно, до конца и с подробным журналом.

Поэтому единственный ручной труд, который я в оффбординге оставляю помимо нажатия кнопки, — вычитывание самого графа перед включением. Гейтинг, идемпотентность и перезапуск с точки сбоя спасают от сбоев инфраструктуры. От неверно нарисованной стрелки не спасает ничего из перечисленного.

Спорьте в комментариях — особенно те, у кого удаление учётной записи при увольнении не автоматизировано вовсе и делается руками по чек-листу. Подозреваю, что у части читателей это осознанный выбор, а не недоработка, и аргументы там могут оказаться сильнее моих.

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