Environments, апрувы и откат
Environment в GitHub — это ворота деплоя. Required reviewers, wait timer и branch-правила ставят прод-джобу на паузу; env-секреты заперты до апрува. А когда деплой ломается, roll-forward часто лучше отката, если уже прошла миграция.
Пятничный хотфикс прошёл CI зелёным, выкатился на staging, и коллега нажал «Approve» на прод-воротах, не глянув в диф, — одиннадцатый апрув за неделю, все рутинные. Этот выкатил конфиг, который направил прод на staging-базу. Сам деплой «успешен»: джоба зелёная, апрув записан. Пейджер сработал через четыре минуты, когда записи начали уходить не туда. Ворота сделали своё дело — поставили паузу и спросили человека, — но человек был натренирован десятью скучными апрувами кликать не глядя. Контроль был; внимание уже потрачено.
Environment — это именованные ворота, а не сервер
В GitHub Actions environment — staging, production — это не машина. Это именованный объект на репозитории, несущий две вещи: protection rules (правила защиты), решающие, когда джоба, целящаяся в него, может запуститься, и секреты и переменные уровня environment, которые резолвятся только внутри джобы, назвавшей этот environment. Джоба подключается одной строкой:
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production # <- привязывает джобу к прод-воротам
steps:
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # резолвится в *прод*-секретВ тот миг, когда джоба объявляет environment: production, становятся истинными три вещи. Она не стартует, пока не пройдут protection rules. Её ссылки secrets.* резолвятся в секреты этого environment, перекрывая одноимённые секреты уровня репозитория. И если задан required reviewer, джоба сидит в состоянии Waiting — видимо приостановлена в UI запуска, — пока кто-то с доступом её не апрувнет. Важнейшее: джоба, ждущая апрува, ещё не может прочитать секреты environment: прод-ключ недостижим, пока ворота не открыты. Это и есть свойство, которое ты покупаешь. Утёкший workflow, который никогда не дойдёт до апрувнутой прод-джобы, никогда не увидит прод-учётки.
Protection rules: ревьюеры, таймеры и кто может деплоить
Когда настраиваешь прод-environment, правильный вопрос не «как добавить ворота?», а «что именно эти ворота останавливают?». GitHub даёт environment небольшой, грубоватый набор правил защиты, и сеньор читает их как контроль радиуса поражения.
| Правило | Что делает | Жёсткий лимит / подвох |
|---|---|---|
| Required reviewers | Джоба ждёт апрува человека (до 6 ревьюеров/команд), прежде чем запуститься | Авто-фейл через 30 дней без апрува; кликнуть должен лишь один из списка |
| Prevent self-review | Тот, кто запустил деплой, не может апрувнуть свой же | Выключено по умолчанию — прямой фикс «штамповки» собственного пуша |
| Wait timer | Задерживает джобу на N минут после триггера (окно «отлёжки» / отмены) | 1–43 200 мин (30 дней); не идёт в оплачиваемое время |
| Deployment branches/tags | Ограничивает, какие ветки/теги могут деплоить в этот environment | например, в production попадают лишь main или теги v* |
На планах Free/Pro/Team эти правила работают только для публичных репозиториев; приватным нужен GitHub Team или Enterprise. И кусается одна особенность дизайна: protection rules цепляются к джобе, а не к workflow. Если три джобы целятся в production, ревьюер апрувит три раза — быстрейшая дорога к усталости от апрувов.
Модель promotion: staging авто, production на паузе
Стандартная форма — единый пайплайн build → staging → production, где staging автоматический, а production — гейтированная promotion того же артефакта. Ты собираешь один раз, выкатываешь ровно эту сборку на staging без ворот, а прод-джоба — зависящая от staging через needs: — встаёт на паузу у своих ворот, пока ревьюер не продвинет её. Добавь concurrency:, чтобы два пуша не гонялись в один environment.
concurrency:
group: deploy-${{ github.ref }} # один прод-деплой за раз на ref
cancel-in-progress: false # дай текущему деплою доехать, следующий — в очередь
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # нет required reviewer -> запускается автоматически
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging # продвигаем ту же сборку
runs-on: ubuntu-latest
environment:
name: production # required reviewers заданы в настройках репо
url: https://app.example.com
steps:
- run: ./deploy.sh productionconcurrency здесь — тихий герой. Без него два мёржа в main с разницей в минуты запускают два прод-деплоя, которые переплетаются — старый артефакт может приземлиться после нового и тихо победить. Ключ группы — любая строка; cancel-in-progress: false даёт безопасный дефолт «доделай текущий деплой, потом следующий», максимум один в работе и один в ожидании.
▸Почему это работает
Почему environment запирает секреты, если можно просто хранить прод-ключ как секрет репозитория? Потому что секрет репо резолвится в любой джобе, включая запущенную из fork-PR или менее доверенного workflow. Секрет environment резолвится только в джобе, которая назвала environment и прошла его protection rules — поэтому прод-учётка недостижима, пока человек не апрувнул деплой из разрешённой ветки. Ворота и скоуп секрета — один механизм: апрув — это ещё и момент, когда секрет становится читаемым.
Когда ломается: откат vs roll-forward
Плохой деплой уехал. Что теперь? Два хода. Откат (rollback): переразвернуть предыдущий заведомо рабочий артефакт — быстро и тривиально, когда пайплайн умеет переподнять деплой-джобу прошлой сборки. Roll-forward (он же fix-forward): пишешь фикс, прогоняешь через тот же гейтированный пайплайн, выкатываешь новую версию поверх.
Откат — инстинкт, но у него острый край: как только деплой прогнал миграцию базы, переразвёртывание старого кода не откатывает схему. Старый бинарник теперь встречает схему, под которую его не писали, — удалённую колонку, переименованную таблицу, бэкфилленный enum. Гайд самого GitLab прямолинеен: нет гарантии, что приложение и база откатятся вместе, так что самая безопасная стратегия — обычно roll-forward. Откатывай код, продвигай вперёд данные. Если миграции аддитивны и обратно совместимы (дисциплина expand/contract), откат кода безопасен — старый код по-прежнему работает с новой схемой. Если они деструктивны, откат может испортить или потерять данные, и форвард-фикс — компенсирующая миграция плюс исправленный код — единственный честный ход.
Джоба с `environment: production` и required reviewer триггернута. До того как кто-либо апрувнул, что верно?
Деплой v2 прогнал миграцию, переименовавшую колонку, затем в новом коде всплыл баг. Как восстанавливаешься?
- 01Что меняется в тот миг, когда джоба объявляет `environment: production`, и почему это делает секреты environment безопаснее секретов репозитория?
- 02После деплоя, прогнавшего деструктивную миграцию, почему roll-forward обычно безопаснее отката к предыдущему артефакту, и когда откат всё-таки норм?
Environment в GitHub Actions — это именованные ворота, а не сервер: он несёт protection rules, решающие, когда целящаяся в него джоба может запуститься, и секреты, которые резолвятся только внутри джобы, назвавшей этот environment. Объявление environment: production держит джобу в Waiting, пока её required reviewer не апрувнет, и прод-секреты остаются нечитаемыми до этого апрува — ровно поэтому секреты environment лучше секретов репо, ведь секрет репо резолвился бы в любой джобе, включая недоверенную. Protection rules — грубоватые контролы радиуса поражения: required reviewers (авто-фейл через 30 дней, апрувнуть должен лишь один), Prevent self-review против штамповки собственного пуша, wait timer от 1 до 43 200 минут как окно отлёжки и ограничения по веткам/тегам на то, кто может деплоить. Они цепляются к джобе, а не к workflow, поэтому три прод-джобы означают три апрува — дорога к усталости от апрувов, где ревьюер, натренированный десятью рутинными кликами, машет одиннадцатому, направившему прод не на ту базу. Стандартный пайплайн собирает один раз и продвигает тот же артефакт staging→production, со staging автоматическим и production гейтированным, и concurrency по ref, чтобы два пуша не загнали устаревший артефакт поверх свежего. Когда деплой ломается, откат — переразвёртывание прошлого артефакта — быстр и верен лишь когда миграции аддитивны и обратно совместимы; как только прошла деструктивная миграция, старый код больше не совпадает со схемой, поэтому делаешь roll-forward с компенсирующей миграцией через те же ворота, а не ставишь на откат или живую хирургию БД. Теперь, когда в инциденте кто-то скажет «давай просто откатимся» — первый вопрос, который ты задаёшь: прошла ли миграция? Этот ответ определяет, поможет откат или создаст второй инцидент поверх первого.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.