Оркестрация сценарием
Это руководство — про управление тем, как именно состояние раскатывается по душам: в каком порядке, с какой степенью параллелизма, на каких хостах. Первичная раскатка (Первый сервис на душах) применяла состояние ко всем хостам сразу; здесь добавляются оркестрационные ключи, которые превращают «применить везде» в «применить волнами с проверкой здоровья между ними».
Справочная грамматика оркестрации — DSL → Scenario; здесь — практические рецепты на её основе.
Оркестрационная дельта в двух словах
Заголовок раздела «Оркестрационная дельта в двух словах»Сценарий получает весь набор блоков задач Destiny (module:, params:, when:, register:, onchanges:, …) плюс оркестрационную дельту — ключи, которых у Destiny нет:
| Ключ | Назначение |
|---|---|
on: | Стабильный таргет: на каких хостах выполнять (по covens — реальным стабильным меткам). Опущен → все члены инкарнации. |
where: | Волатильный фильтр: отобрать хосты по живой проверке или стабильному факту. |
serial: | Волновое (rolling) исполнение: по N хостов за раз. |
run_once: | Выполнить шаг ровно на одном хосте таргета. |
apply: | Вызвать изолированный Destiny вместо инлайн-module:. |
Порядок резолва строгий: сначала on: сужает множество по базе (стабильно), затем where: фильтрует получившееся по живым данным.
Опустите on: — шаг идёт на всех членов инкарнации (членство хоста в инкарнации — самостоятельное отношение, не coven). on: нужен только чтобы сузить таргет: on: keeper или до реального под-coven — стабильной метки, назначенной при онбординге (on: [baremetal]). Covens — это реальные метки (кластер, проект, окружение, датацентр); имя инкарнации к ним не относится.
Таргетинг по стабильным фактам (where:)
Заголовок раздела «Таргетинг по стабильным фактам (where:)»Стабильные факты хоста доступны как soulprint.self.<путь> и известны из реестра — их можно использовать в where: без probe:
# Настроить sysctl только на Debian-семействе- name: Tune kernel on Debian-family hosts module: core.sysctl.present where: soulprint.self.os.family == "debian" params: name: net.core.somaxconn value: "1024"Доступные стабильные факты — soulprint.self.os.family, soulprint.self.os.arch, soulprint.self.sid, soulprint.self.network.primary_ip, soulprint.self.covens и т.д. (полный набор — DSL → Scenario).
probe-роль: волатильная роль через живую проверку
Заголовок раздела «probe-роль: волатильная роль через живую проверку»Стабильные covens сознательно не несут волатильную роль хоста (кто сейчас master, кто replica) — роль меняется от прогона к прогону, а Coven обязан быть стабильным. Чтобы таргетировать по живой роли, её узнают probe-шагом непосредственно перед таргетингом.
Probe — это обычный read-only шаг (core.exec.run или core.cmd.shell) с register: и changed_when: false. Чтобы безопасно таргетировать разрушительную операцию по результату probe, probe обязан утверждать полноту ответа через failed_when::
# probe: узнать живую роль каждого хоста (on: опущен — весь incarnation)- name: Detect actual replication role per host module: core.cmd.shell register: node_role changed_when: false failed_when: size(register.node_role) < incarnation.host_count params: cmd: "the-service role | head -1"failed_when: size(register.node_role) < incarnation.host_count гарантирует, что ответили все хосты. Без этой проверки частичный probe (не все хосты ответили) молча применил бы разрушительную операцию только к «ответившей» части. С проверкой неполный probe делает шаг упавшим, прогон останавливается, состояние не коммитится.
Дальше таргетируем по результату probe:
# применить только к текущим репликам- name: Act only on the current replicas module: core.service.restarted where: register.node_role.stdout == "replica" params: name: the-serviceRolling-раскатка (serial:)
Заголовок раздела «Rolling-раскатка (serial:)»serial: бьёт хосты таргета на последовательные волны: внутри волны — параллельно, волны — строго одна за другой. Падение в волне останавливает раскатку (следующие волны не стартуют).
# по 2 хоста за разserial: 2
# по 25% таргета за волнуserial: "25%"serial: можно ставить на block: — тогда весь блок (несколько задач) прокатывается по одному набору хостов целиком, прежде чем стартует следующая волна. Это и есть идиома «волна = {изменить, проверить здоровье}».
run_once: — один хост на кластер
Заголовок раздела «run_once: — один хост на кластер»run_once: true выполняет шаг ровно на одном хосте таргета (первом по SID) — для операций, которые достаточно сделать единожды на кластер (инициализация, failover, миграция данных):
- name: Trigger failover once, on the current master module: core.cmd.shell where: register.node_role.stdout == "master" run_once: true params: cmd: "the-service failover"serial: и run_once: взаимоисключающи.
Рецепт: rolling-рестарт по onchanges:
Заголовок раздела «Рецепт: rolling-рестарт по onchanges:»Сложим всё вместе — типовая операция «обновить конфиг и перезапустить сервис волнами, только там, где конфиг реально изменился, с проверкой здоровья между хостами». Здесь работают сразу: probe-роль, where:, serial: на блоке, onchanges: и retry/until.
input: max_memory: type: string default: "512mb"
state_changes: sets: max_memory: "${ input.max_memory }" # фиксируем применённый конфиг — это желаемое состояние, а не разовый knob рестарта
tasks: # 1. probe: живая роль каждого хоста (on: опущен — весь incarnation) - name: Detect actual replication role per host module: core.cmd.shell register: node_role changed_when: false failed_when: size(register.node_role) < incarnation.host_count params: cmd: "the-service role | head -1"
# 2. rolling по одному хосту: перерендерить конфиг, рестартнуть ТОЛЬКО при # изменении конфига, дождаться здоровья — и лишь потом следующий хост - name: Rolling config update on replicas, one host at a time where: register.node_role.stdout == "replica" serial: 1 block: - name: Render service config module: core.file.rendered register: svc_conf params: path: /etc/the-service/the-service.conf template: templates/the-service.conf.tmpl vars: max_memory: "${ input.max_memory }" mode: "0640"
# рестарт срабатывает ТОЛЬКО если рендер конфига сообщил changed=true - name: Restart the service because config changed module: core.service.restarted onchanges: [svc_conf] timeout: 30s params: name: the-service
# health-gate перед следующей волной: бюджет времени = count × delay - name: Wait until the host is healthy again module: core.exec.run changed_when: false retry: count: 12 delay: 5s until: contains(register.self.stdout, "status:ok") failed_when: '!contains(register.self.stdout, "status:ok")' params: cmd: the-service args: ["healthcheck"]
# 3. мастер ПОСЛЕДНИМ: запускается только после всех реплик выше. run_once # берёт единственного текущего мастера (probe в момент failover может вернуть # больше одного). В проде сначала делают failover — промоутят свежую реплику, # потом рестартят бывшего мастера; здесь он рестартится на месте — последним. - name: Update and restart the master last, on its own where: register.node_role.stdout == "master" run_once: true block: - name: Render service config on the master module: core.file.rendered register: master_conf params: path: /etc/the-service/the-service.conf template: templates/the-service.conf.tmpl vars: max_memory: "${ input.max_memory }" mode: "0640"
- name: Restart the master because config changed module: core.service.restarted onchanges: [master_conf] timeout: 30s params: name: the-serviceЧто здесь происходит по шагам:
- probe с
failed_when-полнотой узнаёт живую роль всех хостов — без этого rolling мог бы молча обойти невидимую часть душ. serial: 1на блоке катит весь блок (рендер → рестарт → health) по одному хосту за раз; следующий хост стартует, только когда предыдущий прошёл health-gate.onchanges: [svc_conf]перезапускает сервис только там, где конфиг реально изменился: если рендер вернулchanged=false, рестарт пропускается. Идемпотентность сохраняется — холостой прогон не дёргает живой сервис.retry+untilдержит health-gate: до 12 попыток с шагом 5s (бюджет — минута); если хост не оздоровился — шаг падает, и rolling останавливается, не уронив остальные хосты.- Мастер — последним. Все реплики обновляются до того, как финальная задача трогает мастера:
run_once:берёт одного текущего мастера (по тому же probe). Мастер никогда не рестартят в середине раскатки — реплики, которые с него синкаются, встали бы. В проде правильный ход — сначала failover: промоутнуть здоровую реплику в мастера, затем рестартнуть бывшего мастера (теперь реплику); в рецепте он рестартится на месте, чтобы форма была видна яснее.
Запись состояния — cross-host барьер
Заголовок раздела «Запись состояния — cross-host барьер»Что сценарий пишет в incarnation.state, объявляется в state_changes. Коммит состояния — барьер: сценарий дожидается завершения всех задач на всех хостах прогона, и только потом state_changes коммитятся. Если хоть одна задача хоть на одном хосте упала — состояние не коммитится, инкарнация переходит в error_locked. Это инвариант: зафиксированное состояние всегда соответствует факту на хостах.
Что дальше
Заголовок раздела «Что дальше»- DSL → Scenario — нормативная грамматика
on:/where:/serial:/run_once:/apply:, передача данных между хостами. - DSL → Destiny — задачи, requisites (
onchanges/onfail/require),register, рендер шаблонов. - Каталог core-модулей — модули, в том числе keeper-side (
on: keeper). - Мониторинг из коробки — health-проверки и наблюдаемость прогонов.