Шесть ступеней от Git до мониторинга, рабочий код, замеры выкладки на 4000 файлов и чек-листы. Для владельцев сайтов, которым нужно понимать, что просить у подрядчика, и для разработчиков, которым нужны готовые файлы.
Дени БулатовРазработчик Hated.Ru: frontend на React, backend на Python, PostgreSQL
Пятница, вечер. Файлы нового релиза заливают на сервер по FTP, и пока идёт копирование, часть страниц открывается без стилей. В это время кто-то из клиентов оформляет заказ. Через час выясняется, что форма заявки в новой версии не отправляется, и старые файлы уже перезаписаны.
DevOps убирает такие вечера. Это набор практик, при которых код, проверки, выкладка и наблюдение за сайтом работают как один процесс. Изменение проходит автоматические проверки, выкладывается одной командой и при проблеме возвращается к прошлой версии за секунды.
В статье шесть ступеней. Каждую можно внедрять отдельно и оценивать по результату. Конфигурации прошли линтеры и проверку по схемам, скрипт выкладки запущен, замеры сделаны на тестовом стенде. Рядом с каждым примером я указал, что именно проверялось.
Владельцу сайта в конце пригодятся шаблон технического задания и регламент отката. С ними разговор с подрядчиком идёт по делу.
Сайт недоступен при замене каталога на 4000 файлов
Часть запросов получает смесь старых и новых файлов при копировании поверх
Ошибок при переключении ссылки на готовый релиз
Метрик DORA, по которым видно, стало ли лучше
Коротко
Что важно запомнить до чтения
- DevOps объединяет разработку и эксплуатацию в один процесс с общими целями. Начинать нужно с порядка в Git и автоматических проверок. Kubernetes подождёт.
- Три первые ступени, Git, автопроверки и выкладка с откатом, закрывают большую часть проблем сайта на 1С-Битрикс, WordPress или Python.
- Способ выкладки меняет то, что видят посетители. В замере переключение ссылки не дало ни одной ошибки. Замена каталога держала сайт недоступным больше секунды.
- Контейнеры, Terraform и Kubernetes нужны не каждому. В статье есть таблица, по которой можно решить, пора ли.
- Результат измеряют пятью метриками DORA. Скрипт для их подсчёта лежит в разделе про мониторинг.
- Terraform и Docker Hub в России требуют зеркал. Рабочие настройки и даты ограничений приведены с источниками.
01Основы
Что такое DevOps и зачем он сайту
Слово собрано из двух частей: development, разработка, и operations, эксплуатация. Разработчики пишут код и хотят выпускать изменения быстро. Те, кто запускает сайт на сервере, отвечают за стабильность и хотят менять как можно меньше. Пока эти задачи ведут разные люди с разными показателями успеха, каждый релиз превращается в переговоры. DevOps сводит их в один процесс с общими целями.
Термин появился в 2009 году. На конференции O'Reilly Velocity Джон Оллспоу и Пол Хаммонд рассказали, как Flickr выкладывает больше десяти релизов в день. Тогда же бельгийский специалист Патрик Дебуа организовал в Генте первую конференцию DevOpsDays. С тех пор подход прошёл путь от идеи энтузиастов до обычной практики разработки.
Общая ответственность
За работающий сайт отвечают и тот, кто писал код, и тот, кто его запускает. Граница «мы сделали, дальше не наше» исчезает.
Автоматизация
Сборка, проверки, выкладка и настройка серверов выполняются скриптами. Человек принимает решения, а рутину делает конвейер.
Обратная связь по метрикам
Скорость выкладок, число сбоев и время восстановления измеряются. По цифрам видно, стало ли лучше.
Какие проблемы сайта решает DevOps
| Что происходит | Что делает DevOps | Ступень |
|---|---|---|
| Файлы заливают вручную по FTP | Выкладка запускается из репозитория одной командой | 1 и 3 |
| Правки вносят на боевом сервере, потом никто не помнит, что менялось | Весь код в Git, у каждого изменения есть автор и дата | 1 |
| Ошибки находят клиенты | Автоматические проверки срабатывают до выкладки | 2 |
| Во время обновления страницы отдаются с ошибками или без стилей | Новый релиз ставится рядом, ссылка переключается целиком | 3 |
| «У меня работает, на сервере нет» | Разработка, тест и бой собираются из одних и тех же файлов | 4 |
| Сервер настроен руками, пересоздать его страшно | Настройки лежат в репозитории и применяются скриптом | 5 |
| О падении сайта узнают от клиентов | Оповещения приходят в Telegram за минуты | 6 |
Кто такой DevOps-инженер
Это специалист, который отвечает за путь кода от репозитория до работающего сайта. Он настраивает конвейер выкладки, окружения и мониторинг, следит за безопасностью процесса и помогает команде не повторять одни и те же ручные операции. Ему нужны Linux, Git, скрипты на Bash или Python, понимание сетей, контейнеров и облачных платформ.
Не менее важна работа с людьми. С разработчиками он говорит о коде и тестах, с владельцем сайта о рисках, сроках и деньгах. В небольшой студии эти обязанности часто делят между разработчиком и системным администратором, и это нормально: ступени из статьи можно внедрять по очереди без отдельной штатной единицы.
02Сравнение
Как выглядит релиз без процесса и с процессом
Проще всего увидеть разницу на одном и том же изменении. Допустим, на сайте нужно поменять форму заявки. Ниже два сценария для одного и того же сайта: слева привычная пятница, справа спокойный вторник с конвейером.
- Правки на своём компьютереВерсия «финал_2» лежит в папке и в чате. Какая из них актуальная, знает автор.
- Копирование на серверФайлы заливают по FTP или через панель хостинга прямо поверх живого сайта.
- Смесь версийПока идёт копирование, часть посетителей получает новую страницу со старыми скриптами или наоборот.
- Проверка глазамиОткрыли главную, вроде работает. Форму заявки никто не отправлял.
- Сигнал от менеджераЧерез час выясняется, что заявки не приходят. Каждая минута стоит обращений.
- Откатываться нечемСтарые файлы перезаписаны. Прошлую версию ищут в переписке или на чужом компьютере.
- Нет историиЧто именно изменилось в этом релизе, знает тот, кто его заливал.
- Ветка и проверка коллегойИзменение живёт в отдельной ветке Git. Коллега смотрит его в pull request и оставляет замечания.
- Автоматические проверкиПосле отправки запускаются линтер и тесты. Красный статус не даёт слить изменение в основную ветку.
- Тестовая средаКонвейер собирает релиз и ставит его на стенд, который повторяет боевой сайт.
- ПриёмкаЗаказчик или менеджер отправляет тестовую заявку на стенде и подтверждает изменение.
- Выкладка одной командойНовый релиз ставится рядом со старым, затем ссылка на активную версию переключается целиком.
- НаблюдениеМониторинг следит за ошибками и скоростью ответа в первые минуты после выкладки.
- Откат и историяЕсли заявки перестали приходить, одна команда возвращает прошлый релиз. Git хранит, кто и что менял.
Самопроверка: пора ли вашему сайту на DevOps
Отметьте пункты, которые узнаёте в своей работе. Результат подскажет, с какой ступени начать. Отметки сохраняются в вашем браузере, на сервер ничего не уходит.
Признаки, что процесс пора менять
Отмечайте то, что есть у вас сегодня.
Базовая гигиена на месте. Проверьте ступени 4-6: окружения, инфраструктуру кодом и мониторинг. Чаще всего именно там остаются слабые места.
Git, автопроверки и выкладка с откатом закроют большую часть ваших рисков. Каждая ступень даёт результат сама по себе, их можно внедрять по очереди.
Начните со ступени 1 и сразу поставьте мониторинг доступности из ступени 6: так вы узнаете о падении раньше клиентов. Дальше идут автопроверки и выкладка с откатом. Чек-лист и шаблон технического задания в конце статьи помогут обсудить план с подрядчиком.
03Карта
Шесть ступеней от Git до мониторинга
DevOps внедряют по очереди. Каждая ступень даёт результат сама по себе, и сверху её можно надстраивать следующей. Под каждой ступенью указано, как проверить, что она закрыта. Если проверка не проходит, переходить дальше рано.
- 1
Git и порядок в репозитории
Весь код, настройки и миграции лежат в Git. Прямые правки на сервере прекращаются. У каждого изменения есть автор, дата и причина.
Разобрать ступень - 2
Автоматические проверки (CI)
Каждое изменение проходит линтер, тесты и сборку на отдельной машине. Красный статус блокирует слияние в основную ветку.
Разобрать ступень - 3
Выкладка с откатом (CD)
Релиз ставится в отдельный каталог, ссылка на активную версию переключается целиком, прошлые версии лежат рядом.
Разобрать ступень - 4
Окружения и контейнеры
Разработка, тест и бой собираются из одних файлов. Новый разработчик поднимает проект одной командой, а различия окружений видны в конфигурации.
Разобрать ступень - 5
Инфраструктура кодом
Сервер, сеть и настройки описаны файлами. Terraform создаёт ресурсы в облаке, Ansible настраивает систему внутри них.
Разобрать ступень - 6
Мониторинг и оповещения
Недоступность, рост ошибок и медленные ответы приходят в Telegram раньше, чем звонит клиент. Состояние сайта видно на одном экране.
Разобрать ступень
Его нет в списке ступеней намеренно. Kubernetes подходит проектам с несколькими сервисами и переменной нагрузкой, а лендингу и корпоративному сайту он добавит работы больше, чем пользы. Критерии выбора и готовый манифест собраны в отдельном разделе.
04Ступень 1
Git и порядок в репозитории
Git хранит историю каждого изменения и позволяет вернуться к любому состоянию проекта. Для сайта правило одно: исходный код живёт в репозитории, а сервер получает файлы оттуда. Файлы на сервере руками не правят, даже если очень надо срочно.
Договорённости для команды
- Ветка main равна боевому сайту. В неё не пишут напрямую. Работа идёт в коротких ветках: одна задача, одна ветка, закрывается за несколько дней.
- Слияние только через pull request. Коллега смотрит изменение, автоматические проверки зелёные, и только после этого кнопка слияния становится доступной. В настройках репозитория это включается защитой ветки.
- Коммит описывает причину. «Исправил форму заявки: поле телефона не пропускало номера с плюсом» читается через год. «Правки» не читается уже на следующий день.
- Секреты не попадают в репозиторий. Пароли, ключи API и файлы окружения лежат в хранилище секретов. Если пароль всё же попал в историю, его считают скомпрометированным и меняют. Удаление коммита пароль не возвращает.
- Изменения базы данных идут миграциями. Инфоблоки, свойства и формы руками в админке не заводят: на разных средах получится разный результат.
Что смотрит ревьюер
Содержание
- Код делает то, что описано в задаче.
- Нет очевидных ошибок и забытых отладочных строк.
- Крайние случаи обработаны: пустое поле, длинный текст, чужой регион.
Читаемость и безопасность
- Названия понятны без пояснений автора.
- Пароли и адреса боевых серверов в код не вшиты.
- Ввод пользователя экранируется перед выводом и перед запросом в базу.
Хороший комментарий объясняет причину и предлагает вариант. «Здесь запрос в цикле: на странице с 200 товарами это 200 обращений к базе, можно выбрать все товары одним запросом» полезнее, чем «плохо». Отметить удачное решение тоже стоит: это задаёт тон работы команды.
Что не хранить в репозитории
Для проекта на 1С-Битрикс разумно держать в репозитории только свой код, обычно каталог /local/. Ядро ставится из дистрибутива, загрузки лежат на сервере, а в настройках веб-сервера у каждой площадки свои значения. Список ниже собран по рекомендациям Habr (статья 2021 года о правильной настройке площадки Битрикс) и дополнен защитой файлов окружения. Под свой проект его стоит перечитать.
# Ядро 1С-Битрикс ставится из дистрибутива, в репозитории его нет
/bitrix/
# Загрузки посетителей и менеджеров живут на сервере и в бэкапе
/upload/
# Зависимости Composer
/local/vendor/
# Настройки веб-сервера и поисковые файлы на каждой площадке свои
/.htaccess
/web.config
/robots.txt
/*.xml
# Мусор редакторов и логи
*.swp
*.log
# Секреты и локальные настройки
.env
.env.*
!.env.example
Список я проверил в тестовом репозитории: создал файлы по этим путям и спросил Git командой git check-ignore, что он о них думает. Ядро Битрикс, каталог upload, зависимости Composer, robots.txt, sitemap.xml, .htaccess, логи и .env игнорируются. Шаблоны из local/templates, файл local/php_interface.php и пример .env.example остаются под контролем Git.
Для проекта на Python и React набор другой.
# Python
.venv/
__pycache__/
*.pyc
# Node и сборка фронтенда
node_modules/
dist/
# Секреты и локальные настройки
.env
.env.*
!.env.example
# Мусор редакторов и логи
.idea/
.vscode/
*.log
Та же проверка: виртуальное окружение, node_modules, каталог сборки dist, кэш Python, .env и логи игнорируются, исходники и .env.example остаются.
Изменения структуры базы, например новый инфоблок или свойство, переносят между тестовой и боевой площадкой миграциями. В экосистеме Битрикс для этого используют модуль Sprint Migration из Маркетплейса. Для Python-проектов то же делают Alembic или миграции Django. Файлы миграций лежат в репозитории рядом с кодом.
- Любой член команды может ответить, кто, когда и зачем изменил любую строку кода.
- Сравнение каталога на сервере с релизом из репозитория не находит отличий: ручных правок нет.
- Прямое слияние в main запрещено настройками репозитория.
- Секретов в репозитории нет. Проверьте историю поиском по словам
password,secret,token.
05Ступень 2
Автоматические проверки: CI
CI, непрерывная интеграция, означает две вещи. Разработчики отправляют изменения в общий репозиторий часто, почти каждый день. А сервер при каждой отправке сам запускает проверки и сборку. Чем чаще интеграция, тем меньше конфликтов при слиянии и тем раньше видна поломка: если тест краснеет через пять минут после коммита, автор ещё помнит, что менял.
CI, непрерывная доставка и непрерывное развёртывание
Непрерывная интеграция
Изменения регулярно попадают в общий репозиторий и автоматически проверяются: линтер, тесты, сборка.
Непрерывная доставка
После проверок релиз всегда готов к выкладке. Саму выкладку запускает человек одним нажатием.
Непрерывное развёртывание
Выкладка на боевой сайт идёт сама, если все проверки зелёные. Подходит командам с сильными тестами и быстрым откатом.
Для сайтов с заказами и заявками я рекомендую начинать с непрерывной доставки. Тесты покрывают не всё, и последнее слово перед боем лучше оставить человеку, который видит ситуацию целиком: идёт ли сейчас акция, не пик ли нагрузки.
Что проверяет конвейер сайта
- Линтер и форматирование. Находит опечатки в именах переменных, забытые отладочные строки, нарушения стиля.
- Юнит-тесты логики. Расчёт цены и скидки, проверка полей формы, фильтры каталога. Именно здесь ошибки стоят дороже всего.
- Сборка. Для React это
npm run build, для PHP на Composer установка зависимостей без dev-пакетов. Если сборка падает, до сервера релиз не доходит. - Проверка зависимостей.
npm auditиpip-auditпоказывают библиотеки с известными уязвимостями. - Проверка после выкладки на тест. Главная отвечает кодом 200, форма заявки отправляется, API возвращает данные. Такой тест называют дымовым.
Тесты производительности и интерфейса добавляют вторым этапом, когда базовый конвейер работает стабильно.
Пример для GitHub Actions
Конфигурация проверяет Python-проект: ставит зависимости, запускает линтер и тесты, а из основной ветки ещё и собирает образ с тегом по хешу коммита.
name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Окружение
run: |
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
- name: Линтер
run: .venv/bin/ruff check .
- name: Тесты
run: .venv/bin/pytest -q
image:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
IMAGE: registry.example.ru/site/app
steps:
- uses: actions/checkout@v6
- name: Сборка образа с тегом по коммиту
run: docker build -t "$IMAGE:${GITHUB_SHA::12}" .
- name: Вход в реестр и публикация
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
echo "$REGISTRY_TOKEN" | docker login registry.example.ru -u "$REGISTRY_USER" --password-stdin
docker push "$IMAGE:${GITHUB_SHA::12}"
permissions: contents: readдаёт токену конвейера только чтение кода. Больше ему не нужно.concurrencyотменяет предыдущий запуск на той же ветке, когда приходит новый коммит.timeout-minutesпрерывает зависшую сборку, чтобы она не съедала лимит минут.- Пароль реестра берётся из
secretsи передаётся вdocker loginчерез--password-stdin, чтобы он не оказался в списке аргументов процесса. - Тег образа состоит из первых двенадцати символов хеша коммита. По тегу видно, какой код внутри, а тег
latestтакой информации не несёт.
В примере стоит actions/checkout@v6: так читать проще. В боевом проекте GitHub советует закреплять сторонние действия полным хешем коммита вместо плавающего тега и оставлять номер версии комментарием. Тег может быть перенесён, хеш нет. Ещё одна причина обновиться: по уведомлению GitHub, действия на Node.js 20 устаревают: принудительный запуск на Node.js 24 планировался с 2 июня 2026 года, а удаление Node.js 20 с раннеров на 16 сентября 2026 года. Версии вроде checkout@v4 стоит обновить заранее.
Пример для GitLab CI
Три стадии: тесты, сборка образа, выкладка. На тестовую среду релиз уходит сам, на боевую по кнопке.
stages: [test, build, deploy]
default:
image: python:3.12-slim
interruptible: true
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
key:
files: [requirements.txt]
paths: [.cache/pip]
test:
stage: test
script:
- pip install -r requirements.txt -r requirements-dev.txt
- ruff check .
- pytest -q
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
build:
stage: build
image: docker:27
services: [docker:27-dind]
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy_staging:
stage: deploy
environment: staging
script:
- ./deploy.sh staging "$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy_production:
stage: deploy
environment: production
script:
- ./deploy.sh production "$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
rulesзапускает тесты на merge request и на основной ветке, а образ собирает только на основной.when: manualоставляет выкладку на боевой сайт человеку: это и есть непрерывная доставка.- Ключ кэша привязан к файлу
requirements.txt: кэш пересоздаётся, когда меняются зависимости. interruptible: trueразрешает отменять устаревшие запуски, если автоотмена включена в настройках проекта.- Сервис
docker:27-dindзапускает Docker внутри задачи и требует раннера с привилегиями. Для общих раннеров это стоит уточнить заранее.
Оба файла прошли yamllint и проверку по официальным схемам vendor.github-workflows и vendor.gitlab-ci в утилите check-jsonschema. Запускать их на живом GitHub или GitLab я не мог: нужен репозиторий с раннерами и реестр. Перед боевым использованием файл стоит прогнать в вашем проекте.
Какой инструмент выбрать
| Инструмент | Где живёт конфигурация | Где выполняется | Когда подходит |
|---|---|---|---|
| GitHub Actions | Файлы в .github/workflows | Раннеры GitHub или ваши собственные | Код уже на GitHub, нужен быстрый старт |
| GitLab CI/CD | Файл .gitlab-ci.yml | Раннеры GitLab, облачного или развёрнутого у вас | Хочется держать код и сборку в своём контуре, на своих серверах |
| Jenkins | Задания и конвейеры в самом Jenkins | Ваш сервер, расширения через плагины | Сложные нестандартные сценарии и полный контроль над сервером сборки |
Инструмент выбирают по тому, где живёт код. Если репозиторий на GitLab, конвейер проще держать там же. Отдельный Jenkins ради двух сайтов обычно избыточен: его самого нужно обновлять, защищать и мониторить.
Когда конвейер идёт дольше 10-15 минут, его перестают ждать и начинают обходить. Тогда проверки делят на быстрые, которые идут на каждый коммит, и долгие, которые запускаются раз в сутки или перед релизом.
Как понять, что ступень закрыта
- Слить изменение с красным статусом невозможно.
- Время от коммита до результата проверок известно и укладывается в 10-15 минут.
- Секреты лежат в хранилище конвейера. В файлах репозитория их нет.
- Образ или архив релиза собирает конвейер на отдельной машине.
06Ступень 3
Выкладка без простоя и откат за секунды
Вся ступень держится на одной идее: активный релиз это ссылка. Новая версия распаковывается рядом со старой, проверяется, и только потом ссылка переключается на неё. Старые релизы остаются на диске, поэтому откат превращается в такое же переключение ссылки в обратную сторону.
Схема каталогов
/var/www/site/
├── current -> releases/20261017-141502 # ссылка на активный релиз
├── releases/
│ ├── 20261017-141502/ # активный
│ ├── 20261016-093011/ # предыдущий, для отката
│ └── 20261015-170544/
└── shared/ # живёт между релизами
├── upload/ # загрузки посетителей
└── .env # настройки и секреты сервера
Конвейер распаковывает архив в новую папку releases/дата-время, подключает к ней общие каталоги из shared и в самом конце меняет ссылку current. Веб-сервер смотрит в current, поэтому видит либо старую версию целиком, либо новую целиком. Промежуточного состояния нет.
Попробуйте переключение сами
Это модель схемы. Кнопка «Выложить» создаёт новый релиз рядом и переносит ссылку на него, кнопка «Откатить» возвращает ссылку на ближайший релиз старше активного. Хранятся пять последних релизов, как в скрипте ниже.
Скрипт выкладки и отката
Скрипт работает на обычном Linux-сервере без дополнительных программ. Он принимает архив с готовым релизом, ставит его рядом с предыдущими, подключает общие каталоги и атомарно переключает ссылку.
#!/usr/bin/env bash
# Выкладка релиза по схеме releases/ + симлинк current. Откат за секунду.
set -euo pipefail
APP_DIR="${APP_DIR:-/var/www/site}"
KEEP="${KEEP:-5}"
usage() {
echo "Использование: $0 deploy <архив.tar.gz> | rollback | list" >&2
exit 2
}
deploy() {
local archive="$1"
local release
release="$APP_DIR/releases/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$release" "$APP_DIR/shared"
tar -xzf "$archive" -C "$release"
# Общие каталоги живут вне релиза: загрузки и конфиги не теряются при выкладке
ln -sfn "$APP_DIR/shared/upload" "$release/upload"
ln -sfn "$APP_DIR/shared/.env" "$release/.env"
# Атомарное переключение: новый симлинк создаётся рядом и переименовывается
ln -sfn "$release" "$APP_DIR/current.new"
mv -Tf "$APP_DIR/current.new" "$APP_DIR/current"
# Хранить только последние KEEP релизов, текущий не трогаем
find "$APP_DIR/releases" -mindepth 1 -maxdepth 1 -type d | sort -r \
| tail -n +"$((KEEP + 1))" | awk -v cur="$release" '$0 != cur' | xargs -r rm -rf
echo "Выложен релиз: $release"
}
rollback() {
local current previous
current="$(readlink -f "$APP_DIR/current")"
# Имена релизов это даты, поэтому сортировка по имени идёт по времени.
# Берём ближайший релиз старше текущего: повторный откат идёт ещё глубже.
previous="$(find "$APP_DIR/releases" -mindepth 1 -maxdepth 1 -type d | sort -r \
| awk -v cur="$current" '$0 < cur { print; exit }')"
if [ -z "$previous" ]; then
echo "Предыдущего релиза нет" >&2
exit 1
fi
ln -sfn "$previous" "$APP_DIR/current.new"
mv -Tf "$APP_DIR/current.new" "$APP_DIR/current"
echo "Откат на: $previous"
}
case "${1:-}" in
deploy) [ $# -eq 2 ] || usage; deploy "$2" ;;
rollback) rollback ;;
list) find "$APP_DIR/releases" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -r ;;
*) usage ;;
esac
set -euo pipefailостанавливает скрипт на первой ошибке. Выкладка, которая молча дошла до середины, хуже остановки с понятным сообщением.- Ссылка создаётся под именем
current.newи затем переименовывается вcurrentкомандойmv -Tf. Переименование выполняется одним системным вызовом, поэтому посетитель не застаёт момент, когда ссылки нет. - Имена релизов состоят из даты и времени. Сортировка по имени совпадает с порядком по времени, и откат находит ближайший релиз старше текущего. Повторный откат уходит ещё глубже и к сломанной версии не возвращается.
- Очистка оставляет последние
KEEPрелизов и не трогает активный.
Вот реальный прогон: две выкладки, откат и список релизов. Скрипт запущен в песочнице.
Первая версия скрипта падала с ненулевым кодом, пока релизов было меньше, чем значение KEEP: фильтр очистки ничего не находил, и конвейер считал выкладку неудачной, хотя ссылка уже переключилась. Нашёл на тесте, исправил и прогнал восемь выкладок подряд: все коды нулевые, на диске ровно пять релизов. Поэтому скрипты выкладки стоит проверять сразу в двух состояниях: на пустом сервере и после нескольких релизов.
Замер: чем отличаются способы выкладки
Чтобы не спорить о том, «как лучше», я сравнил четыре способа на одинаковом релизе. В нём 4002 файла: страница, скрипт и 4000 файлов по 2,4 КБ. Пока идёт выкладка, отдельный процесс отправляет около тысячи запросов в секунду. Каждый запрос читает страницу, скрипт и случайный файл из assets и проверяет, что все три из одного релиза. Ошибкой считаются три случая: файла нет, файл пустой (его как раз копируют) и версии не совпали. Цифры ниже это медианы трёх прогонов в пересчёте на одну выкладку.
| Способ | Окно с ошибками на выкладку | Что видит посетитель |
|---|---|---|
| Удалить каталог и скопировать новый | 1,24 с | Около 98% запросов за это время получают ошибку: файлов нет |
| Скопировать новые файлы поверх живого сайта | 0,5 с | Около половины запросов получают смесь старых и новых файлов, а в среднем 0,65 запроса на выкладку попадает на пустой файл |
| Переключить ссылку командой ln -sfn | 0 с | Ошибок нет. Переключение занимает около 2 мс |
| Ссылка и явное переименование mv -T | 0 с | Ошибок нет. Переключение занимает около 5 мс |
Код четырёх способов совпадает с тем, что делают на практике: заменить папку, залить файлы поверх, подменить ссылку.
# Четыре способа выкладки, которые сравнивались в замере.
# $1 = корень сайта, $2 = имя релиза (A или B), в каталоге rel/ лежат готовые релизы.
# 1. Удалить каталог и скопировать новый (так делают при замене папки по FTP)
d_rm_cp() { rm -rf "$1/current"; cp -r "rel/$2" "$1/current"; }
# 2. Скопировать новые файлы поверх живого сайта
d_overwrite() { cp -r "rel/$2/." "$1/current/"; }
# 3. Переключить ссылку на готовый релиз
d_ln_sfn() { ln -sfn "rel/$2" "$1/current"; }
# 4. То же с явным переименованием: ссылка создаётся рядом и подменяется одним rename
d_atomic() { ln -sfn "rel/$2" "$1/current.new"; mv -Tf "$1/current.new" "$1/current"; }
Тестовый стенд: 2 ядра Intel Xeon 2,8 ГГц, Linux, файловая система ext4, Python 3.11, coreutils 9.4. Три прогона по 20 выкладок для способов с копированием и по 600 для переключения ссылки. Полный скрипт замера приведён под спойлером ниже, его можно запустить у себя. Результат измеряет только слой файлов. Веб-сервера, PHP, базы данных и кэша браузера в замере нет, поэтому на реальном сайте картина будет сложнее.
Полный скрипт замера: run.sh и reader.py
Запуск: FILES=4000 SLOW=20 FAST=600 ./run.sh. Нужны Bash и Python 3, рядом должен лежать reader.py. Скрипт создаёт временный каталог, сам его удаляет и печатает по строке на способ.
#!/usr/bin/env bash
# Сравнение способов выкладки: что видит посетитель во время обновления.
set -euo pipefail
cd "$(dirname "$0")"
FILES=${FILES:-4000}
SLOW=${SLOW:-20}
FAST=${FAST:-300}
BASE=$(mktemp -d)
trap 'rm -rf "$BASE"' EXIT
make_release() { # $1 = каталог релиза, $2 = номер версии
mkdir -p "$1/public/assets"
echo "version-$2" > "$1/public/index.html"
echo "version-$2" > "$1/public/app.js"
python3 - "$1/public/assets" "$FILES" "$2" <<'PY'
import sys, os
d, n, v = sys.argv[1], int(sys.argv[2]), sys.argv[3]
blob = ("v" + v + " ") * 600
for i in range(n):
with open(os.path.join(d, f"f{i}.txt"), "w") as f:
f.write(blob)
PY
}
# релизы A и B, которые выкладываем по очереди
make_release "$BASE/rel/A" A
make_release "$BASE/rel/B" B
run_method() { # $1 = название, $2 = функция выкладки, $3 = число выкладок
local name=$1 fn=$2 DEPLOYS=$3
local root="$BASE/$name"; mkdir -p "$root"
# стартовое состояние
case $name in
rm_cp|overwrite) cp -r "$BASE/rel/A" "$root/current" ;;
*) mkdir -p "$root/releases"; cp -r "$BASE/rel/A" "$root/releases/A"; ln -sfn "$root/releases/A" "$root/current" ;;
esac
local stop="$root/STOP"; rm -f "$stop"
FILES=$FILES python3 reader.py "$root" "$stop" "$root/result.json" &
local rp=$!
sleep 0.3
local t0 t1
t0=$(date +%s.%N)
for ((i=0; i<DEPLOYS; i++)); do
if (( i % 2 == 0 )); then rel=B; else rel=A; fi
$fn "$root" "$rel"
done
t1=$(date +%s.%N)
touch "$stop"; wait "$rp"
python3 - "$name" "$root/result.json" "$t0" "$t1" "$DEPLOYS" <<'PY'
import json, sys
name, p, t0, t1, d = sys.argv[1], sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), int(sys.argv[5])
r = json.load(open(p))
bad = r['missing'] + r['empty'] + r['mixed']
per_req_ms = 1000 * r['elapsed'] / r['reads']
print(f"{name:10s} выкладок={d:>4d} запросов={r['reads']:>7d} missing={r['missing']:>6d} empty={r['empty']:>4d} mixed={r['mixed']:>4d} "
f"| на выкладку: ошибок {bad/d:7.1f}, окно {bad*per_req_ms/d:7.1f} мс | выкладка {1000*(t1-t0)/d:6.1f} мс")
PY
}
d_rm_cp() { rm -rf "$1/current"; cp -r "$BASE/rel/$2" "$1/current"; }
d_overwrite() { cp -r "$BASE/rel/$2/." "$1/current/"; }
d_ln_sfn() { ln -sfn "$BASE/rel/$2" "$1/current"; }
d_atomic() { ln -sfn "$BASE/rel/$2" "$1/current.new"; mv -Tf "$1/current.new" "$1/current"; }
echo "файлов в релизе: $((FILES+2)), поток около 1000 запросов в секунду"
run_method rm_cp d_rm_cp "$SLOW"
run_method overwrite d_overwrite "$SLOW"
run_method ln_sfn d_ln_sfn "$FAST"
run_method atomic d_atomic "$FAST"
import os, sys, json, time, random
root, stop, out = sys.argv[1], sys.argv[2], sys.argv[3]
idx = os.path.join(root, 'current/public/index.html')
js = os.path.join(root, 'current/public/app.js')
FILES = int(os.environ.get('FILES', '4000'))
INTERVAL = 0.001 # один запрос в миллисекунду: около 1000 запросов в секунду
def one():
"""Один визит: страница, затем скрипт. Возвращает ok, missing, empty или mixed."""
try:
with open(idx, 'rb') as f:
a = f.read()
except OSError:
return 'missing'
if not a:
return 'empty'
try:
with open(js, 'rb') as f:
b = f.read()
except OSError:
return 'missing'
if not b:
return 'empty'
# страница и случайный файл из assets должны быть из одного релиза
try:
with open(os.path.join(root, 'current/public/assets/f%d.txt' % random.randrange(FILES)), 'rb') as f:
c = f.read()
except OSError:
return 'missing'
if not c:
return 'empty'
v = a.strip()[-1:]
return 'ok' if (b.strip()[-1:] == v and c[1:2] == v) else 'mixed'
cnt = {'ok': 0, 'missing': 0, 'empty': 0, 'mixed': 0}
t_start = time.perf_counter()
while not os.path.exists(stop):
t = time.perf_counter()
cnt[one()] += 1
left = INTERVAL - (time.perf_counter() - t)
if left > 0:
time.sleep(left)
elapsed = time.perf_counter() - t_start
reads = sum(cnt.values())
json.dump(dict(reads=reads, elapsed=elapsed, **{k: v for k, v in cnt.items() if k != 'ok'}), open(out, 'w'))
В нескольких прогонах от 0 до 21 запроса на 600 переключений получили страницу из одного релиза и файл из другого: запрос попал ровно между двумя чтениями. На реальном сайте это страница, открытая за миг до переключения, которая запрашивает скрипт уже из нового релиза. Защита описана ниже, в ловушке про кэш и ресурсы. Поэтому слова «ноль простоя» без оговорок я не использую: на уровне файлов окно исчезает, а в целом выкладка зависит ещё от базы и кэшей.
Про время копирования. В замере 4000 файлов дали около 1,2 с при замене каталога. Окно у способов с копированием растёт вместе с числом файлов в релизе, а у переключения ссылки не зависит от него. Распаковка нового релиза при этом идёт рядом с живым сайтом и посетителей не затрагивает.
Четыре ловушки, о которых вспоминают после первого сбоя
OPcache держит старый код
На PHP-сайте после переключения ссылки может продолжать работать прежний код. OPcache привязан к пути файла, а путь current остался тем же, поэтому смену релиза он не замечает.
Решений два. Перезагрузить PHP-FPM или сбросить кэш сразу после переключения. Или отдавать PHP реальный путь релиза через переменную $realpath_root в настройках nginx. Во втором случае стоит проверить, не меняются ли значения в $_SERVER.
База данных не переключается ссылкой
Файлы меняются за миллисекунды. Схема базы так быстро не меняется. Миграции делают в два шага: сначала добавляют колонки и таблицы, которые не мешают старому коду, потом выкладывают новый код, а лишнее удаляют только в следующем релизе.
Так откат кода не упирается в схему. Перед миграцией делают бэкап и проверяют, что он восстанавливается. Откат кода базу назад не возвращает.
Страница и ресурсы из разных релизов
Посетитель загрузил страницу до переключения и запросил скрипт после. Если в новом релизе у файла другое имя, получится ошибка 404.
Защита простая. Называйте ресурсы с хешем, например app.4f9c2a.js, и складывайте их в общий каталог shared/assets, не удаляя старые сразу. Тогда страница любого релиза найдёт свой файл.
Общие каталоги и права
Загрузки и файл окружения живут в shared и подключаются к каждому релизу ссылками. Если забыть подключить upload, новый релиз стартует без картинок.
Скрипт подключает их сам, а проверка после выкладки должна открыть страницу с картинкой. Файлы релиза принадлежат пользователю, от имени которого работает веб-сервер, иначе первая же запись в кэш упадёт с ошибкой доступа.
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;Строки приведены по описанию в разборе OPcache и симлинк-выкладок (ссылка в источниках). Перед применением проверьте конфигурацию командой nginx -t на своём сервере: у меня nginx в песочнице нет.
Стратегии выкладки: от одного сервера до нескольких
| Стратегия | Как работает | Когда нужна |
|---|---|---|
| Переключение ссылки | Релиз ставится рядом, ссылка меняется целиком | Один сервер, обычный сайт или интернет-магазин |
| Rolling | Серверы обновляются по одному, пока остальные принимают запросы | Несколько серверов за балансировщиком |
| Blue-Green | Две полные среды. Новая версия проверяется без нагрузки, затем весь трафик переключается, старая среда остаётся в резерве | Нужно проверить релиз на реальных данных до переключения и иметь мгновенный откат |
| Canary | Новая версия сначала уходит на небольшую часть серверов, например на 1-2%, потом расширяется | Большой трафик и рискованные изменения |
Переключение ссылки это упрощённый Blue-Green для одного сервера: два каталога, одна ссылка. В услуге по внедрению CI/CD у Hated.Ru откаты и выкладка на боевой сайт входят в расширенный пакет, а Blue-Green и Canary в пакет Enterprise. Подробнее в разделе про дорожную карту.
Как понять, что ступень закрыта
- Откат выполняется одной командой, и вы его уже делали на тестовой среде.
- На сервере лежат последние пять релизов.
- Перед каждой миграцией базы делается бэкап, и восстановление из него проверено.
- Вы знаете, что делает ваш PHP-кэш после переключения ссылки.
07Ступень 4
Окружения и контейнеры: одинаково у разработчика, на тесте и в бою
У сайта минимум три места жизни: компьютер разработчика, тестовый сервер и боевой. Если там стоят разные версии PHP, Python или PostgreSQL, ошибки всплывают только в бою. Фраза «у меня работает» родом отсюда.
Контейнер собирает окружение из файла. Версия языка, пакеты и настройки описаны в Dockerfile, а несколько сервисов, например приложение и база, поднимаются вместе через Docker Compose. Один набор файлов и одна команда запуска.
Нужны ли контейнеры вашему сайту
Сайту на Python, Node.js или Go контейнеры почти всегда полезны. С 1С-Битрикс картина другая. Часто сайт живёт на готовой виртуальной машине или хостинге с настроенным стеком, и контейнеры добавляют слой, который кто-то должен обслуживать. Рабочее правило такое: если проект одинаково собирается и запускается у всей команды, контейнеры можно отложить. Если он «собирается у одного разработчика», контейнеры закрывают проблему быстрее всего.
Четыре понятия, с которых начинается разговор
| Понятие | Что это | Пример |
|---|---|---|
| Dockerfile | Текстовая инструкция сборки образа | Взять Python 3.12, поставить зависимости, скопировать код |
| Образ | Результат сборки: приложение и всё, что ему нужно, в одном неизменяемом пакете | registry.example.ru/site/app:3f9c2a1b7d40 |
| Контейнер | Запущенный экземпляр образа. Из одного образа запускают сколько нужно копий | Три копии сайта за балансировщиком |
| Compose-файл | Описание нескольких контейнеров, сетей и томов для одной команды запуска | Приложение и PostgreSQL на ноутбуке разработчика |
Dockerfile для Python-сайта
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
RUN useradd --create-home --uid 10001 app
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --chown=app:app . .
USER 10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
- Два этапа сборки. На первом ставятся зависимости, на втором в образ попадает только результат. Образ получается меньше, в нём нет кэша pip.
- Зависимости копируются раньше кода. Пока
requirements.txtне менялся, Docker берёт этот слой из кэша, и сборка проходит за секунды. - Пользователь без прав администратора. Приложение запускается под номером 10001. Если в нём найдут уязвимость, права внутри контейнера у атакующего будут ограничены.
- Проверка здоровья. Каждые 30 секунд Docker открывает адрес
/health. Три ответа подряд без успеха, и контейнер помечается нездоровым.
Dockerfile прошёл линтер hadolint. Первая версия получила замечания по пользователю и по форме команды проверки здоровья, я их исправил. Образ я не собирал: в песочнице, где готовилась статья, нет демона Docker. Запустите docker build у себя и посмотрите размер образа.
Compose для приложения и базы
services:
app:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
Приложение стартует после того, как база отвечает на pg_isready: за это отвечает depends_on с условием service_healthy. Пароль приходит из переменной DB_PASSWORD, которая лежит в файле .env рядом с compose-файлом. В репозиторий этот файл не попадает, в .gitignore он уже есть. Том pgdata хранит данные базы вне контейнера, поэтому пересоздание контейнера их не стирает.
Файл разобран командой docker compose config без ошибок. Сами контейнеры я не поднимал по той же причине, что и образ.
docker compose config # проверить файл и подстановку переменных
docker compose up -d --build # собрать образ и поднять приложение с базой
docker compose ps # состояние контейнеров и healthcheck
docker compose logs -f app # журнал приложения
docker compose down # остановить и удалить контейнеры, том с данными остаётся
- 30 мая 2024Docker Hub стал недоступен для пользователей из России. Docker сослался на то, что компания американская и обязана соблюдать правила экспортного контроля США. Об этом написал Security Lab в тот же день.
- 3 июня 2024В GitVerse появилось бета-зеркало Docker Hub для российских пользователей, инструкция по подключению лежит по адресу dh-mirror.gitverse.ru.
Если сервер или конвейер тянут базовые образы напрямую с Docker Hub, сборка может упасть в самый неподходящий момент. Что с этим делают на практике:
- Подключают зеркало в настройках демона Docker, параметр
registry-mirrorsв файлеdaemon.json. - Заводят свой реестр: GitLab Container Registry или реестр вашего облака. Базовые образы копируют в него заранее, а в Dockerfile пишут свой адрес.
- Закрепляют версии образов, например
python:3.12-slimиpostgres:17. Тегlatestзавтра может оказаться другой версией.
Условия доступа менялись и могут измениться снова, поэтому я привожу даты событий. Проверьте доступность со своего сервера до того, как от неё зависит релиз.
Четыре ошибки с контейнерами
Тег latest в боевом образе
Сегодня собралась одна версия базового образа, через месяц другая, и пересборка прошлого релиза даст уже другой результат. Тегом образа делайте короткий хеш коммита, как в примере для GitHub Actions выше.
Секреты внутри образа
В образе остаётся всё, что в него скопировали, включая файл .env. Добавьте его в .dockerignore, а пароли передавайте при запуске через переменные окружения или хранилище секретов.
Данные базы без тома
Контейнер можно удалить и создать заново в любой момент. Всё, что лежало внутри него без тома, исчезает вместе с ним. Базу, загрузки и сессии выносите в тома или во внешнее хранилище.
Всё в одном контейнере
Приложение, база и веб-сервер в одном образе заставляют перезапускать всё сразу и мешают масштабировать нужную часть. Один процесс на контейнер, связь между ними описывает Compose.
Как понять, что ступень закрыта
- Чистая машина запускает проект командой
docker compose up, и никто не вспоминает, какие пакеты поставить. - Версии базовых образов закреплены, а тега
latestв боевых файлах нет. - Секретов в образе нет, это видно по
.dockerignoreи по содержимому слоёв. - Базовые образы доступны с сервера, где идёт сборка: через зеркало или свой реестр.
08Ступень 5
Инфраструктура кодом: сервер, который можно пересоздать
Сервер, настроенный вручную, знает один человек. Какие пакеты стоят, какие строки поправлены в конфигах, где лежат ключи. Когда сервер падает, второй такой же собирают по памяти и по истории команд в терминале.
Инфраструктура кодом описывает сервер, сеть и настройки файлами. Файлы лежат в Git рядом с кодом сайта: у каждой правки есть автор и причина, а новую среду создаёт запуск команды.
Две задачи и два инструмента
| Инструмент | Что делает | Пример задачи |
|---|---|---|
| Terraform | Создаёт ресурсы в облаке и помнит, что создал: сети, виртуальные машины, диски, DNS | Поднять сервер 2 ядра и 4 ГБ в Яндекс Облаке |
| Ansible | Настраивает систему внутри машины по SSH, агент на сервер ставить не нужно | Установить nginx, включить файрвол, положить конфиг сайта |
Связка работает по очереди. Terraform создаёт машину и выдаёт её адрес, Ansible подключается к этому адресу и приводит систему в нужное состояние.
Terraform: виртуальная машина в Яндекс Облаке
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
# Версию закрепите после первого terraform init: она запишется в .terraform.lock.hcl
}
}
required_version = ">= 1.6"
}
provider "yandex" {
zone = "ru-central1-a"
folder_id = var.folder_id
}
variable "folder_id" {
type = string
description = "Идентификатор каталога в Yandex Cloud"
}
variable "ssh_public_key" {
type = string
description = "Публичный ключ для доступа к серверу"
}
data "yandex_compute_image" "ubuntu" {
family = "ubuntu-2404-lts"
}
resource "yandex_vpc_network" "main" {
name = "site-network"
}
resource "yandex_vpc_subnet" "main" {
name = "site-subnet-a"
zone = "ru-central1-a"
network_id = yandex_vpc_network.main.id
v4_cidr_blocks = ["10.10.0.0/24"]
}
resource "yandex_compute_instance" "web" {
name = "site-web-1"
platform_id = "standard-v3"
resources {
cores = 2
memory = 4
core_fraction = 50
}
boot_disk {
initialize_params {
image_id = data.yandex_compute_image.ubuntu.id
size = 30
}
}
network_interface {
subnet_id = yandex_vpc_subnet.main.id
nat = true
}
metadata = {
ssh-keys = "deploy:${var.ssh_public_key}"
}
}
output "web_ip" {
value = yandex_compute_instance.web.network_interface[0].nat_ip_address
}
В файле три группы ресурсов: сеть с подсетью, виртуальная машина с двумя ядрами, 4 ГБ памяти и диском 30 ГБ, и вывод её внешнего адреса. Образ системы берётся по семейству ubuntu-2404-lts, то есть при создании подставится актуальная сборка. Идентификатор каталога и публичный ключ передаются переменными: их кладут в файл terraform.tfvars вне Git или в переменные окружения с префиксом TF_VAR_.
Версию провайдера закрепляют после первого terraform init. Она записывается в файл .terraform.lock.hcl, и этот файл коммитят в репозиторий.
terraform init # скачать провайдер, записать версии в .terraform.lock.hcl
terraform fmt -check # единый стиль файлов
terraform validate # проверка синтаксиса и ссылок между ресурсами
terraform plan -out=tfplan # что будет создано, изменено и удалено
terraform apply tfplan # применить именно этот план
Правило одно: apply всегда выполняется по сохранённому плану, а план читает второй человек. Так видно, что будет создано, изменено и удалено, до того как это произойдёт.
Состояние Terraform
Terraform хранит соответствие между вашими файлами и реальными ресурсами в файле состояния. Его нельзя терять и править руками. Для команды состояние держат в общем хранилище с блокировкой: пока один человек выполняет apply, второй запустить его не сможет. В руководстве Яндекс Облака состояние лежит в Object Storage, а блокировку обеспечивает база YDB. Руководство предупреждает, что начиная с Terraform 1.11 блокировка через Document API объявлена устаревшей, поэтому сверяйте его с версией, которую используете. Ссылка в источниках.
Файл разобран парсером HCL без ошибок. Команды terraform init и plan я не запускал: им нужен доступ к облаку и к зеркалу провайдера. Перед первым запуском сверьте имена ресурсов и допустимые значения параметров (platform_id, zone, core_fraction) с документацией провайдера.
Лицензия и доступность в России
- 10 августа 2023HashiCorp объявила о смене лицензии Terraform с MPL 2.0 на Business Source License. Последней версией под MPL стала 1.5.7 от 7 сентября 2023 года, версия 1.6.0 вышла 4 октября 2023 года уже под BUSL 1.1.
- 20 сентября 2023Linux Foundation приняла в свой состав OpenTofu, форк Terraform 1.5.7 под лицензией MPL 2.0. Первый стабильный релиз, OpenTofu 1.6.0, вышел 10 января 2024 года.
- 28 августа 2024OpenTofu закрыл доступ к своему реестру с российских адресов и убрал из него провайдеры российских облаков: Yandex Cloud, Cloud.ru и Rustack. Причиной назвали соблюдение санкций.
HashiCorp тоже закрыла российским адресам доступ к своему реестру. Типичное проявление: terraform init падает с ошибкой 403 Forbidden при обращении к registry.terraform.io. Для Яндекс Облака есть зеркало, которое подключается файлом ~/.terraformrc (в Windows это terraform.rc в каталоге %APPDATA%):
provider_installation {
network_mirror {
url = "https://terraform-mirror.yandexcloud.net/"
include = ["registry.terraform.io/*/*"]
}
direct {
exclude = ["registry.terraform.io/*/*"]
}
}
После этого выполняют terraform init -upgrade. Содержимое файла совпадает с рекомендацией из документации Яндекс Облака на день написания статьи. Яндекс предупреждает, что зеркало может быть недоступно, поэтому закрепляйте версии и храните нужные провайдеры у себя на случай сбоя.
Я не юрист, поэтому условия лицензии BUSL для вашего случая стоит обсудить с юристом. OpenTofu остаётся вариантом для тех, кому нужна открытая лицензия, но его реестр для российских провайдеров закрыт, и для российских облаков проще работать с Terraform и зеркалом.
Ansible: настройка сервера
Ansible описывает желаемое состояние сервера. Повторный запуск ничего не ломает: если nginx уже установлен и конфиг не менялся, задача отмечается как выполненная без изменений. Пример настраивает веб-сервер: ставит пакеты, кладёт конфигурацию сайта, включает файрвол.
[web]
site-web-1 ansible_host=203.0.113.10 ansible_user=deploy
- name: Настройка веб-сервера
hosts: web
become: true
roles:
- web
- name: Пакеты
ansible.builtin.apt:
name: [nginx, ufw, fail2ban]
state: present
update_cache: true
cache_valid_time: 3600
- name: Конфиг сайта
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-available/site.conf
mode: "0644"
notify: Перезагрузить nginx
- name: Включить сайт
ansible.builtin.file:
src: /etc/nginx/sites-available/site.conf
dest: /etc/nginx/sites-enabled/site.conf
state: link
notify: Перезагрузить nginx
- name: Разрешить SSH, HTTP и HTTPS
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: ["22", "80", "443"]
- name: Включить файрвол
community.general.ufw:
state: enabled
policy: deny
Задача «Конфиг сайта» и задача «Включить сайт» при изменении сообщают обработчику, что nginx нужно перезагрузить. Обработчик срабатывает один раз в конце прогона.
- name: Перезагрузить nginx
ansible.builtin.service:
name: nginx
state: reloaded
server {
listen 80;
server_name {{ site_domain }};
root /var/www/site/current/public;
index index.php index.html;
}
Значение site_domain подставляется из переменных роли. Пароли и ключи хранят в зашифрованном файле Ansible Vault.
ansible-playbook -i inventory.ini site.yml --syntax-check # синтаксис
ansible-playbook -i inventory.ini site.yml --check --diff # пробный прогон: что изменилось бы
ansible-playbook -i inventory.ini site.yml # настроить сервер
Пробный прогон с флагами --check --diff показывает, какие файлы изменились бы и как. Его стоит делать перед каждым запуском на боевом сервере.
Плейбук прошёл проверку синтаксиса в ansible-core 2.19 с коллекцией community.general. На реальном сервере я его не запускал: для этого нужна машина с доступом по SSH.
Как понять, что ступень закрыта
- Тестовую среду можно удалить и поднять заново одним запуском, вручную ничего не настраивается.
- Файлы Terraform и Ansible лежат в Git, а состояние хранится в общем хранилище с блокировкой.
- Пробный прогон Ansible на боевом сервере показывает ноль изменений: сервер совпадает с описанием.
- Секреты лежат в хранилище или в зашифрованном виде, в репозитории открытых паролей нет.
09Оркестрация
Kubernetes: когда он нужен сайту
Kubernetes управляет контейнерами на нескольких серверах: запускает нужное число копий, заменяет упавшие, раскатывает новую версию по очереди и добавляет копии при росте нагрузки. Система выросла из внутреннего проекта Google, а в 2014 году Google открыл её исходный код.
Возможности большие, и цена у них такая же. Кластер нужно обновлять, следить за ним и разбираться в его сетях. Поэтому сначала вопрос, а нужен ли он вам.
Нужен ли Kubernetes: таблица для решения
| Ситуация | Вывод |
|---|---|
| Лендинг или корпоративный сайт на Tilda, WordPress, Битриксе | Не нужен. Хватит выкладки по ссылке и мониторинга |
| Интернет-магазин на одном сервере с предсказуемой нагрузкой | Не нужен. Docker Compose и один хороший сервер покрывают задачу |
| Несколько сервисов, у каждого своя нагрузка и свой цикл релизов | Стоит рассмотреть. Оцените, кто будет обслуживать кластер |
| Нагрузка скачет в несколько раз в течение суток или сезона | Подходит: автомасштабирование как раз для этого |
| Несколько команд выкладывают независимо друг от друга | Подходит: у каждой команды свои пространства имён и права |
Если большинство строк вашего сайта в верхней части таблицы, пропустите этот раздел и вернитесь к нему, когда проект вырастет.
Словарь Kubernetes для владельца сайта
Под (Pod)
Минимальная единица запуска: один или несколько контейнеров, которые работают вместе и делят сеть и хранилище.
Deployment
Описывает, сколько копий приложения должно работать и как их обновлять. Он же хранит историю ревизий для отката.
Service
Постоянный адрес для набора подов. Поды приходят и уходят, адрес остаётся.
Ingress
Правила входящего трафика: какой домен и путь ведёт на какой сервис.
HPA
Автомасштабирование по нагрузке: добавляет поды, когда процессор загружен, и убирает, когда нагрузка спадает.
Узел (Node)
Виртуальная или физическая машина, на которой запускаются поды. Управляющие узлы руководят кластером, рабочие несут нагрузку.
Манифест для сайта: три копии, обновление без простоя, автомасштабирование
apiVersion: apps/v1
kind: Deployment
metadata:
name: site
labels:
app: site
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: site
template:
metadata:
labels:
app: site
spec:
containers:
- name: site
image: registry.example.ru/site/app:3f9c2a1b7d40
ports:
- containerPort: 8000
envFrom:
- secretRef:
name: site-env
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: site
spec:
selector:
app: site
ports:
- port: 80
targetPort: 8000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: site
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: site
minReplicas: 3
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- Три копии.
replicas: 3держит три пода. Если один упадёт, Kubernetes создаст новый. - Обновление по очереди. Параметры
maxUnavailable: 0иmaxSurge: 1означают: сначала запускается один новый под, и только когда он готов, старый останавливается. Число рабочих копий в процессе обновления не падает ниже трёх. - Проверки готовности и жизни. Readiness-проба решает, можно ли отправлять трафик на под. Liveness-проба перезапускает под, который завис. Обе опрашивают адрес
/health. - Ресурсы. Запрос процессора и памяти нужен планировщику, чтобы разместить под, а лимит памяти защищает соседей от утечки.
- Секреты. Переменные окружения берутся из секрета
site-env. Сам секрет создают отдельной командой, в Git он не лежит. - Автомасштабирование. HPA держит от 3 до 8 подов и добавляет копии, когда средняя загрузка процессора превышает 70% от запрошенного: в манифесте запрошено 100m, то есть десятая часть ядра на под.
kubectl apply -f k8s/app.yaml # применить манифест
kubectl rollout status deployment/site # дождаться, пока новые поды станут готовыми
kubectl get pods -l app=site # посмотреть поды приложения
kubectl rollout history deployment/site # какие ревизии хранятся
kubectl rollout undo deployment/site # вернуть предыдущую ревизию
Команда kubectl rollout undo возвращает предыдущую ревизию Deployment. Параметр revisionHistoryLimit: 5 в манифесте хранит пять последних, как схема с пятью релизами из раздела про выкладку.
Манифест прошёл проверку по схемам Kubernetes 1.37 (инструмент kubernetes-validate). В кластере я его не запускал. Адрес образа registry.example.ru учебный, подставьте адрес своего реестра.
Сколько живёт версия Kubernetes
Кластер нельзя поставить один раз и забыть. Минорные версии выходят примерно три раза в год, и каждая получает исправления около года. По данным kubernetes.io на 17 октября 2026 года поддерживаются четыре минорные версии:
| Версия | Последний патч | Окончание поддержки |
|---|---|---|
| 1.37 | 1.37.1 от 15 сентября 2026 | 28 октября 2027 |
| 1.36 | 1.36.5 от 15 сентября 2026 | 28 июня 2027 |
| 1.35 | 1.35.9 от 15 сентября 2026 | 28 февраля 2027 |
| 1.34 | 1.34.12 от 15 сентября 2026 | 27 октября 2026 |
Версия 1.34 перестанет получать исправления безопасности через десять дней после даты, на которую написана статья. Кластер на ней придётся обновлять в ближайшие недели. Для небольшого проекта это постоянная статья расходов: время инженера на обновления раз в несколько месяцев.
Если нагрузка действительно требует Kubernetes, а своих инженеров нет, берут управляемый кластер у облачного провайдера. Управляющие узлы обслуживает провайдер, а вам остаются манифесты, мониторинг и выбор момента для обновления версии. Настройка управляемого кластера входит в услуги Hated.Ru по облаку, стоимость указана на странице услуги.
Как понять, что ступень закрыта, если Kubernetes вам нужен
- Обновление приложения не роняет число рабочих копий ниже заданного, это видно по
kubectl rollout status. - Откат на предыдущую ревизию вы уже делали на тестовом кластере.
- У каждого пода есть пробы готовности и жизни, а также запросы и лимиты ресурсов.
- В команде есть человек, который знает дату окончания поддержки текущей версии кластера и план обновления.
10Ступень 6
Мониторинг и оповещения: узнать о проблеме раньше клиента
Выкладка и откат закрывают половину задачи. Вторая половина состоит в том, чтобы вовремя заметить, что с сайтом что-то не так. Если о падении узнают из звонка клиента, мониторинга у вас нет, даже если на сервере стоит десяток панелей.
Четыре сигнала, с которых начинают
В книге Google о надёжности сервисов перечислены четыре «золотых сигнала». Для сайта они читаются так:
Задержка
За сколько секунд отвечает страница. Успешные и ошибочные ответы считают отдельно.
Трафик
Сколько запросов приходит в секунду. Резкое падение так же важно, как рост.
Ошибки
Доля ответов 5xx и упавших запросов к базе. Скачок означает дефект или сбой железа.
Насыщение
Сколько ресурсов занято: процессор, память, диск. Высокое значение говорит, что сервер подходит к пределу.
Что проверять на небольшом сайте
| Уровень | Что смотрим | Когда сработает |
|---|---|---|
| Снаружи | Открывается ли главная и страница заявки с внешней точки | Сайт не отвечает 2 минуты |
| Приложение | Доля ошибок 5xx, время ответа | Ошибок больше 5% за 5 минут или медленные ответы 10 минут |
| Сервер | Процессор, память, диск | Диск закончится меньше чем за сутки |
| Заявки | Приходят ли письма и записи в CRM с формы | За рабочий день нет ни одной заявки |
Последняя строка выходит за рамки техники. Форма может отвечать кодом 200 и при этом не доставлять заявки. Для сайта, который живёт на заявках, это главная проверка, и её придумывают отдельно под ваш проект.
Prometheus, Alertmanager и Grafana
Стандартная связка для небольших проектов состоит из трёх частей. Prometheus сам опрашивает источники метрик по расписанию и хранит их как временные ряды. Alertmanager принимает от него сработавшие правила и рассылает уведомления, в том числе в Telegram, почту и Slack. Grafana рисует графики и дашборды.
Prometheus не ждёт, пока приложение пришлёт данные: он сам обходит адреса, которые отдают метрики. Такие адреса называются экспортёрами. Для сервера используют node exporter, для внешней проверки сайта blackbox exporter, для приложения добавляют свою библиотеку метрик.
Актуальная ветка Prometheus 3. Версия 3.0 вышла 14 ноября 2024 года и стала первым мажорным релизом за семь лет: в ней появился новый интерфейс, поддержка UTF-8 в названиях метрик и приём метрик по протоколу OpenTelemetry. Если ставите систему сейчас, берёте актуальную версию.
Правила оповещений для сайта
groups:
- name: site
rules:
- alert: SiteDown
expr: probe_success{job="blackbox-site"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Сайт {{ $labels.instance }} не отвечает"
- alert: HighErrorRate
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Доля ответов 5xx выше 5% за 5 минут"
- alert: SlowResponses
expr: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) > 1.5
for: 10m
labels:
severity: warning
annotations:
summary: "95-й перцентиль времени ответа выше 1,5 секунды"
- alert: DiskFillingUp
expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24 * 3600) < 0
for: 30m
labels:
severity: warning
annotations:
summary: "Диск на {{ $labels.instance }} закончится меньше чем за сутки"
- SiteDown. Внешняя проверка не проходит две минуты подряд. Параметр
forзащищает от ложных тревог при секундном сбое сети. - HighErrorRate. Доля ответов 5xx выше 5% в течение пяти минут. Это ранний признак сломанного релиза и повод проверить, не пора ли откатываться.
- SlowResponses. 95-й перцентиль времени ответа выше 1,5 секунды десять минут. Он показывает, как отвечает медленная пятая часть запросов, и среднее значение этого не скрывает.
- DiskFillingUp. Функция
predict_linearсмотрит на динамику за шесть часов и предупреждает, если диск заполнится меньше чем за сутки. Забитый диск остаётся частой причиной падения сайтов, и предупредить о нём можно заранее.
Уведомления идут в Telegram-чат команды. Критичные срабатывания стоит отделить от предупреждений: SiteDown будит дежурного, а SlowResponses ждёт утра.
Все четыре выражения разобраны парсером PromQL без ошибок. Работу правил на реальном Prometheus с реальным трафиком я не проверял: названия метрик http_requests_total и http_request_duration_seconds_bucket зависят от библиотеки метрик вашего приложения, их нужно сверить.
Логи: что нужно маленькому сайту
Метрики показывают, что что-то сломалось. Логи отвечают, что именно. Для небольшого сайта достаточно писать журналы nginx и приложения в файлы с ротацией и уметь быстро их открыть. Когда серверов становится несколько, логи собирают в одном месте.
Классический вариант называется ELK: Elasticsearch хранит и ищет, Logstash обрабатывает, Kibana показывает. Он требует заметного объёма памяти, поэтому для небольшого проекта часто выбирают более лёгкие решения вроде Grafana Loki. По лицензии у Elastic в 2024 году произошла смена курса: 29 августа 2024 года компания объявила, что Elasticsearch и Kibana снова открытые: к прежним лицензиям добавляется вариант AGPL.
Установили конвейер, стало ли лучше? Ответ даёт исследовательская программа DORA. Первые четыре метрики она описала в 2014 году, а в отчёте 2024 года добавила пятую. Метрики делятся на две группы.
Что показывает пропускную способность
- Время изменения. Сколько проходит от коммита в систему контроля версий до работы в бою.
- Частота выкладок. Сколько выкладок за период или какой промежуток между ними.
- Время восстановления. За какое время исправляют выкладку, которая потребовала немедленного вмешательства.
Что показывает, как проходят выкладки
- Доля неудачных выкладок. Какая часть выкладок потребовала немедленного вмешательства.
- Доля внеплановых выкладок. Какая часть выкладок случилась внепланово из-за инцидента в бою. Это пятая метрика, добавленная в 2024 году.
Считать их можно без дорогих платформ: достаточно журнала выкладок. Для каждой выкладки записывают время коммита, время выкладки, признак неудачи, время восстановления и признак внеплановой выкладки. Скрипт на Python ниже считает все пять метрик из такого файла и использует только стандартную библиотеку.
commit_at,deployed_at,failed,recovered_at,unplanned
2026-09-14T10:05,2026-09-14T15:30,0,,0
2026-09-16T11:20,2026-09-16T16:00,0,,0
2026-09-18T09:40,2026-09-18T14:10,1,2026-09-18T14:35,0
2026-09-18T14:40,2026-09-18T15:00,0,,1
2026-09-22T10:15,2026-09-22T15:45,0,,0
2026-09-23T12:00,2026-09-23T16:20,0,,0
2026-09-25T09:30,2026-09-25T13:50,0,,0
2026-09-29T10:45,2026-09-29T15:10,0,,0
2026-10-01T11:10,2026-10-01T16:40,0,,0
2026-10-06T10:00,2026-10-06T15:20,0,,0
2026-10-08T09:50,2026-10-08T14:30,0,,0
2026-10-09T12:30,2026-10-09T17:05,0,,0
"""Пять метрик DORA из журнала выкладок. Только стандартная библиотека Python 3.9+."""
import csv
import sys
from datetime import datetime
from statistics import median
def ts(value):
return datetime.fromisoformat(value) if value else None
def hm(minutes):
return f"{int(minutes // 60)} ч {int(minutes % 60):02d} мин"
def pct(part, whole):
return f"{100 * part / whole:.1f}".replace(".", ",") + f"% ({part} из {whole})"
rows = []
with open(sys.argv[1], newline="", encoding="utf-8") as f:
for r in csv.DictReader(f):
rows.append({
"commit": ts(r["commit_at"]),
"deploy": ts(r["deployed_at"]),
"failed": r["failed"] == "1",
"recovered": ts(r["recovered_at"]),
"unplanned": r["unplanned"] == "1",
})
first = min(r["deploy"] for r in rows)
last = max(r["deploy"] for r in rows)
weeks = max((last - first).days / 7, 1)
lead = [(r["deploy"] - r["commit"]).total_seconds() / 60 for r in rows]
recovery = [(r["recovered"] - r["deploy"]).total_seconds() / 60
for r in rows if r["failed"] and r["recovered"]]
failed = sum(r["failed"] for r in rows)
unplanned = sum(r["unplanned"] for r in rows)
print(f"Период: {first:%d.%m.%Y} - {last:%d.%m.%Y}")
print(f"Частота выкладок: {len(rows) / weeks:.1f}".replace(".", ",") + f" в неделю ({len(rows)} за период)")
print(f"Время от коммита до боя, медиана: {hm(median(lead))}")
print(f"Доля неудачных выкладок: {pct(failed, len(rows))}")
print(f"Время восстановления, медиана: {hm(median(recovery)) if recovery else 'не было сбоев'}")
print(f"Доля внеплановых выкладок: {pct(unplanned, len(rows))}")
Данные в файле учебные: двенадцать выкладок за неполные четыре недели, я составил их для демонстрации. На своих данных цифры получатся другими. Смотрите на динамику: если время от коммита до боя снижается, а доля неудачных выкладок не растёт, процесс улучшается.
Пороги для «хорошо» и «плохо» я не привожу: они меняются от года к году и зависят от отрасли. Сравнивайте себя с собой за прошлый квартал.
Как понять, что ступень закрыта
- Остановите сервис на тестовой среде: оповещение в Telegram придёт через 2-3 минуты.
- Состояние сайта видно на одном экране: доступность, ошибки, время ответа и свободное место.
- Есть проверка доставки заявок, которая смотрит дальше кода ответа.
- Журнал выкладок ведётся, и раз в месяц вы смотрите пять метрик DORA.
11Безопасность
Безопасность внутри процесса: что проверять на каждом шаге
Автоматизация ускоряет выкладку и так же быстро выкладывает ошибки, в том числе опасные. Поэтому проверки безопасности встраивают в тот же конвейер: так их не забывают, и стоят они копейки по сравнению с утечкой базы клиентов. Для этого подхода есть название DevSecOps.
Чтобы не утонуть в списках, ниже шесть мест, где у небольшого сайта обычно и случаются проблемы.
Секреты
Пароли и ключи лежат в хранилище секретов конвейера или в зашифрованном файле. В репозитории их нет, а в журнале сборки значения скрыты: в GitHub это происходит само, в GitLab переменную нужно отметить как masked. Если пароль попал в Git, его считают утёкшим и меняют.
Права конвейера
Токену конвейера выдают минимум прав. В примере для GitHub Actions это строка permissions: contents: read: сборка читает код и больше ничего не может.
Сторонние действия
Готовые шаги из каталога GitHub Actions выполняются с доступом к вашему репозиторию. Рекомендация GitHub: закреплять их по полному хешу коммита и читать исходный код. В примере в статье тег для читаемости, а в рабочем проекте хеш подставьте сами.
Зависимости
Библиотеки сайта проверяются сканером уязвимостей при каждой сборке. Известная дыра в старой версии пакета закрывается обновлением, пока на неё не пришли.
Доступы людей
Доступ к серверу и панели управления у каждого свой, по ключам. Уволившегося сотрудника отключают в тот же день, и для этого есть список мест, где у него были права.
Резервные копии
Копия, из которой ни разу не восстанавливались, работает только на бумаге. Раз в квартал восстановите сайт из резервной копии на тестовой среде и запишите, сколько это заняло.
Внешние данные в конвейере
Название ветки, заголовок pull request и тексты комментариев приходят от людей и могут содержать любые символы. Если подставить их в команду оболочки как есть, получится дыра: чужой текст выполнится на вашей сборочной машине. Данные из таких источников передают через входные параметры шага или через переменные окружения, а в строку команды не склеивают.
Удаление файла следующим коммитом ничего не меняет: пароль остаётся в истории. Порядок действий такой: сначала смените пароль или ключ у сервиса, потом уберите его из кода, и только после этого чистите историю, если без этого не обойтись. В обратном порядке вы теряете время, пока утёкший ключ ещё работает.
Как понять, что безопасность встроена в процесс
- Конвейер получает минимум прав и берёт пароли из хранилища секретов.
- Сканер зависимостей запускается автоматически, и красный результат блокирует слияние.
- Восстановление из резервной копии проверено в этом квартале, время записано.
- Список людей с доступом к серверу актуален, а ключи у каждого свои.
12Люди
Команда и договорённости: кто за что отвечает
Инструменты работают, когда за ними стоят понятные правила. В небольшом проекте все роли часто совмещает один-два человека, но список обязанностей всё равно стоит выписать. Тогда ясно, кто принимает решение о релизе и кто идёт разбираться, если сайт упал.
| Роль | За что отвечает | Что должна уметь показать |
|---|---|---|
| Заказчик или владелец продукта | Приоритеты, приёмка релиза, решение «выкладываем» | Список изменений в ближайшем релизе и критерии готовности |
| Разработчик | Код, автотесты, ревью чужих изменений | Pull request с зелёными проверками |
| DevOps-инженер | Конвейер, окружения, мониторинг, откат | Откат на прошлый релиз за минуту и работающие оповещения |
| Тестировщик | Сценарии проверки, заявки и оплата перед релизом | Отметку о том, какие сценарии пройдены на тесте |
Договорённости, которые дешевле всего внедрить
Релизы по вторникам и средам
В пятницу вечером сбой стоит дороже всего: команда уходит, а клиенты остаются. Если конвейер надёжный, релиз можно сделать в любой день. Пока он не такой, выбирайте начало недели и рабочие часы.
Маленькие релизы
Один релиз с одним изменением проще проверить и откатить, чем «большое обновление на месяц». Когда что-то ломается, виновата короткая и понятная правка.
Один человек на релиз
Он нажимает кнопку, смотрит на графики первые 15 минут и решает, откатываться ли. Его имя пишут в канале команды до выкладки.
Постмортем без поиска виноватых
После каждого сбоя команда пишет короткий разбор: что случилось, почему не заметили раньше, что меняем. Цель разбора: найти слабое место процесса. Шаблон в конце статьи.
Доска для релизов
Достаточно простой доски в формате Kanban с колонками «В работе», «На ревью», «Проверка на тесте», «Готово к релизу», «В бою». Число задач в работе ограничивают: когда колонка заполнена, начинать новое нельзя, пока не закончено старое. Так застрявшие изменения видны сразу и не копятся в ветках.
Первые две недели после внедрения конвейера закладывайте время на разбор странных случаев. Тесты окажутся то слишком строгими, то слишком мягкими, а скрипт отката найдёт ещё один путь, который стоило проверить. Это обычное начало, и оно проходит, когда конвейером пользуются каждую неделю.
13Выбор
Что внедрять вашему типу сайта
Нужны ли все шесть ступеней и в каком порядке их проходить, зависит от проекта. Таблица подскажет, с чего начать и что отложить.
| Тип сайта | С чего начать | Что добавить позже | Что чаще всего лишнее |
|---|---|---|---|
| Лендинг или сайт на Tilda | Мониторинг доступности и заявок, учёт домена и DNS, копия содержимого | Проверка скорости загрузки после каждой правки | Контейнеры, Terraform, Kubernetes: выкладывать нечего, конструктор делает это сам |
| Корпоративный сайт на WordPress | Git для своей темы и плагинов, тестовая копия, бэкапы и проверка восстановления | CI с проверкой стиля и выкладка по ссылке | Kubernetes |
| Сайт или магазин на 1С-Битрикс | Git для каталога /local/, миграции базы, выкладка по ссылке, мониторинг заявок и заказов | CI с автотестами оплаты и заказа, тестовая среда, как у боевой | Kubernetes при одном сервере |
| Веб-приложение на Python или Node.js | Все ступени с первой по четвёртую, мониторинг ошибок и времени ответа | Terraform и Ansible, несколько окружений | Kubernetes, пока нагрузка помещается на один сервер |
| Маркетплейс или высоконагруженный сервис | Все шесть ступеней, метрики DORA, постмортемы | Blue-Green и Canary, автомасштабирование | Ручные операции на боевых серверах, их нужно убирать первыми |
Если сомневаетесь, к какой строке относится ваш проект, начните с общих двух шагов. Первый шаг: проверить, что код лежит в Git и что вы умеете откатиться. Второй шаг: настроить оповещение о недоступности сайта. Они занимают несколько дней и дают результат на любом типе проекта.
14План
План внедрения на 90 дней и как это делает Hated.Ru
Все шесть ступеней разом никто не внедряет. Реалистичный план растягивается на квартал, и каждый этап заканчивается проверкой.
| Срок | Что делаем | Результат, который можно проверить |
|---|---|---|
| Недели 1-2 | Код в Git, правила веток, оповещение о недоступности сайта | На сервере нет файлов, которых нет в репозитории. Остановка сайта на тесте приводит сообщение в Telegram |
| Недели 3-4 | Автоматические проверки: линтер, тесты, сборка | Намеренно сломанный тест блокирует слияние |
| Недели 5-6 | Выкладка по ссылке и откат одной командой | Откат выполнен на тестовой среде меньше чем за минуту |
| Недели 7-10 | Контейнеры и одинаковые окружения | Чистая машина запускает проект командой docker compose up |
| Недели 11-13 | Terraform, Ansible, журнал выкладок и метрики DORA | Тестовая среда пересоздаётся одним запуском, пять метрик посчитаны за месяц |
Пять ошибок внедрения
- Начинать с Kubernetes. Сначала порядок в Git и откат, оркестрация потом и только если нужна.
- Автоматизировать хаос. Если выкладка без процесса работает через раз, скрипт будет через раз выкладывать неправильно.
- Писать тесты на всё сразу. Начните с трёх сценариев: открывается главная, отправляется заявка, проходит заказ.
- Доверять откату, который ни разу не запускали. Проверенный откат отличается от задуманного на бумаге.
- Считать внедрение законченным. Конвейеру нужен владелец, который следит за зависимостями и ключами.
Какие работы есть в Hated.Ru
Если разобраться самим некогда, внедрение CI/CD мы делаем под ключ. Цены и состав пакетов ниже взяты со страницы услуги на 17 октября 2026 года, окончательную стоимость мы называем после разбора проекта.
от 40 000 ₽
Срок: от 1 недели. Простой конвейер для небольшого проекта: настройка сборки, автоматические тесты, деплой на тестовую среду, документация процессов и обучение команды.
от 70 000 ₽
Срок: от 2 недель. Всё из базового пакета, плюс деплой на продакшн, стратегии откатов, контейнеризация Docker, мониторинг и алерты, уведомления в Telegram и Slack.
от 120 000 ₽
Срок: от 3 недель. Всё из расширенного пакета, плюс мультисредовый деплой, Blue-Green и Canary, инфраструктура как код.
Отдельные задачи и доработки считаем по часам, от 3 500 ₽ в час. В первый месяц после внедрения мы на связи для правок, это указано на странице услуги.
| Смежная услуга | Стоимость | Срок |
|---|---|---|
| Аудит инфраструктуры с расчётом и рекомендациями | от 30 000 ₽ | от 5 дней |
| Настройка мониторинга и алертов | от 25 000 ₽ | от 3 дней |
| Резервное копирование и регламент восстановления | от 30 000 ₽ | от 3 дней |
| Сопровождение инфраструктуры | от 25 000 ₽ в месяц | постоянно |
Подробности и состав каждого пакета описаны на страницах внедрения CI/CD, подбора и настройки инфраструктуры и настройки Яндекс Облака и VK Cloud.
15Чек-листы
Три чек-листа, которые стоит распечатать
Первый нужен перед выкладкой, второй сразу после неё, третий при приёмке работ у подрядчика. Отметки сохраняются в вашем браузере, на сервер ничего не отправляется. Кнопка «Сбросить» очищает список перед следующим релизом.
Чек-лист перед релизом
Пройдите список за 10 минут до выкладки. Пункт без галочки означает, что выкладку стоит отложить.
Первые 15 минут после релиза
Приёмка работ у подрядчика
Отмечайте то, что вы видели своими глазами.
16Шаблоны
Готовые шаблоны: ТЗ подрядчику, регламент отката, разбор сбоя
В квадратных скобках стоят места, которые нужно заменить своими данными. Кнопка «Копировать» переносит текст целиком.
Техническое задание на внедрение CI/CD
Показывает подрядчику, что вы знаете, что покупаете, и даёт вам список для приёмки. Все критерии проверяются руками, поэтому спорных моментов при сдаче почти нет.
Техническое задание на внедрение CI/CD 1. Проект Название и адрес сайта: [название, адрес] Платформа: [1С-Битрикс, WordPress, Python, другое] версии [укажите] Где размещён: [хостинг, VPS, облако], доступ у: [кого] 2. Как выкладываем сегодня [Кто, как часто, чем: FTP, панель, скрипт]. Последний сбой при выкладке: [дата и что случилось]. 3. Что нужно получить - Код в репозитории [GitHub, GitLab, другое] с правилами веток и обязательным ревью - Автоматические проверки на каждое изменение: [линтер, тесты, сборка] - Выкладка на тестовую среду и на боевой сайт одной командой - Откат на прошлый релиз меньше чем за минуту, [N] последних релизов на сервере - Оповещения в [Telegram, Slack, почту] о недоступности, росте ошибок и медленных ответах 4. Окружения Тест и бой [отличаются, совпадают] по версиям [PHP, Python, БД]. Тестовая среда должна быть доступна по адресу [адрес]. 5. Безопасность и доступы Секреты хранятся в хранилище конвейера. Аккаунты на нашу компанию. Права минимальные. Список людей с доступом: [список]. 6. Документация и обучение Инструкция по выкладке и откату, схема окружений, список секретов без значений. Обучение [N] сотрудников с выкладкой тестового изменения. 7. Критерии приёмки - Намеренно сломанный тест блокирует слияние - Откат показан вживую на тестовой среде - Тестовая остановка сайта приводит оповещение за [2-3] минуты - Проверено восстановление из резервной копии 8. Сроки и бюджет Срок: [дата]. Бюджет: [сумма или вилка]. Поддержка после запуска: [период]. 9. Что просим в ответе подрядчика План работ по этапам, список используемых инструментов, состав команды, стоимость отдельно по этапам, что в стоимость не входит.
Регламент отката
Короткая страница, которую открывает ответственный за релиз, когда что-то идёт не так. Пороги подставьте под свой сайт. Если порог уже превышен, обсуждать нечего: откат делается сразу, а причину ищут после.
Регламент отката сайта [название] Когда откатываемся без обсуждений - Сайт недоступен дольше [2] минут - Доля ответов 5xx выше [5]% дольше [5] минут после выкладки - Форма заявки или оплата не работает - Ответственный за релиз не может за [10] минут назвать причину и срок исправления Кто решает Ответственный за релиз: [имя, телефон]. Заместитель: [имя, телефон]. Решение принимает один человек, согласования не требуются. Порядок действий 1. Написать в рабочий чат: «Откатываю релиз [номер], причина: [кратко]». 2. Выполнить на сервере: ./deploy.sh rollback 3. Проверить главную, форму заявки и корзину. Открыть графики ошибок и времени ответа. 4. Если миграция базы была в релизе: сверить, совместим ли старый код со схемой. Восстановление базы из бэкапа выполняется только по решению [роль] и только после остановки записи. 5. Написать в чат: «Откат выполнен в [время], сайт работает на релизе [номер]». После отката - Завести разбор сбоя по шаблону в течение [1] рабочего дня - Исправление выкладывается как новый релиз, возврат на неисправный релиз запрещён - Дату и причину отката внести в журнал выкладок Контакты Сервер: [адрес, доступ]. Хостинг или облако: [поддержка]. Подрядчик: [телефон].
Разбор сбоя
Пишется в течение рабочего дня после инцидента, пока детали свежи. Объём хватит на страницу.
Разбор сбоя [дата] Краткое резюме [Одно-два предложения: что произошло и как долго] Влияние Сколько минут сайт был недоступен или работал с ошибками: [N]. Сколько заявок или заказов потеряно по оценке: [N]. Кого затронуло: [все, часть посетителей, один раздел]. Хронология (время по часам сервера) [чч:мм] выкладка релиза [номер] [чч:мм] первый сигнал: [оповещение, звонок клиента, график] [чч:мм] решение откатить [чч:мм] сайт восстановлен Причины Что сработало как триггер: [изменение, сбой, нагрузка]. Почему оно дошло до боя: [какая проверка отсутствовала или не сработала]. Что сработало хорошо [откат, оповещение, инструкция] Что не сработало [нет теста на сценарий, оповещение пришло поздно, нет доступа] Действия № | Что сделать | Ответственный | Срок | Статус 1 | [действие] | [имя] | [дата] | [открыто] Правило разбора: ищем слабое место процесса. Имя автора правки в тексте не обязательно, если оно ничего не добавляет к причине.
17FAQ
Частые вопросы про DevOps для сайта
Что такое DevOps простыми словами?
Это способ работать, при котором разработка и эксплуатация сайта выстроены в один процесс. Код хранится в Git, проверяется автоматически, выкладывается одной командой и откатывается за секунды, а за состоянием сайта следят метрики и оповещения. Результат для владельца: обновления выходят без ночных авралов, а о сбое команда узнаёт раньше клиентов.
С чего начать, если сайт на 1С-Битрикс?
С двух шагов. Первый: положить в Git собственный код, обычно каталог /local/, и закрыть ядро и каталог upload в .gitignore. Второй: поставить оповещение о недоступности сайта и формы заявки. Дальше добавляют миграции базы, чтобы инфоблоки и свойства одинаково появлялись на тесте и в бою, затем выкладку по ссылке и автопроверки.
Сколько стоит внедрение CI/CD?
На странице услуги Hated.Ru на 17 октября 2026 года указаны три пакета. Базовый от 40 000 ₽ и от 1 недели: сборка, автотесты, деплой на тестовую среду. Расширенный от 70 000 ₽ и от 2 недель: добавляется деплой на продакшн, стратегии откатов, Docker, мониторинг и алерты. Enterprise от 120 000 ₽ и от 3 недель: мультисредовый деплой, Blue-Green и Canary, инфраструктура как код. Отдельные задачи по часам, от 3 500 ₽. Итоговую цену мы называем после разбора проекта.
Сколько времени занимает внедрение?
Подрядчик с готовыми наработками делает базовый конвейер от одной недели, расширенный от двух, полный от трёх. Своими силами за квартал проходят все шесть ступеней в темпе, который показан в плане на 90 дней. Самая долгая часть чаще всего касается людей: нужно договориться о правилах веток и ревью.
В чём разница между CI, непрерывной доставкой и непрерывным развёртыванием?
CI это автоматическая сборка и проверка каждого изменения. Непрерывная доставка добавляет к этому готовый к выкладке релиз, который выкладывают по решению человека. При непрерывном развёртывании выкладка на боевой сайт происходит сама, когда проверки пройдены. Для большинства сайтов подходит доставка: кнопка остаётся у ответственного за релиз.
Нужен ли Docker сайту на Битриксе или WordPress?
Часто нет. Если у всей команды проект собирается одинаково на виртуальной машине или хостинге, контейнеры добавляют обслуживание без заметной пользы. Они нужны, когда окружения расходятся, новому разработчику сложно поднять проект или у приложения несколько сервисов.
Нужен ли Kubernetes интернет-магазину?
Магазину на одном сервере с предсказуемой нагрузкой Kubernetes обычно не нужен: хватает Docker Compose и хорошего сервера с резервной копией. Его стоит рассматривать, когда сервисов несколько, нагрузка сильно скачет или несколько команд выкладывают независимо. Таблица для решения есть в разделе про Kubernetes.
Можно ли выкладывать обновления без простоя?
На уровне файлов да: в замере на 4000 файлов переключение ссылки не дало ни одной ошибки. Замена каталога держала сайт недоступным около 1,2 секунды. Полный ноль зависит ещё от базы данных, кэша PHP и браузера посетителя, поэтому слова «ноль простоя» без оговорок я не использую. Для строгих требований есть Blue-Green и Canary.
Как откатиться, если в релизе была миграция базы?
Откат кода базу назад не возвращает. Поэтому миграции делают в два шага: сначала добавляют колонки и таблицы, которые не мешают старому коду, потом выкладывают новый код, а лишнее удаляют в следующем релизе. Перед миграцией делают бэкап и проверяют, что он восстанавливается.
Работают ли Terraform и Docker Hub в России?
С ограничениями. С 30 мая 2024 года Docker Hub недоступен для пользователей из России, поэтому нужны зеркало или свой реестр. HashiCorp закрыла российским адресам доступ к реестру провайдеров, для Яндекс Облака есть зеркало. Реестр OpenTofu с 28 августа 2024 года закрыт для российских адресов и без провайдеров российских облаков. Условия могут измениться, проверяйте их до релиза.
Как понять, что процесс стал лучше?
По пяти метрикам DORA: время от коммита до боя, частота выкладок, время восстановления, доля неудачных выкладок и доля внеплановых выкладок. Скрипт для подсчёта есть в разделе про мониторинг. Сравнивайте текущий месяц с прошлым.
Чем DevOps-инженер отличается от системного администратора?
Системный администратор обслуживает серверы и сеть. DevOps-инженер отвечает за путь кода от коммита до боя: конвейер, окружения, откат и мониторинг, и описывает инфраструктуру кодом. На небольшом проекте эти роли нередко совмещает один человек.
Кто отвечает за конвейер после внедрения?
Нужен владелец: человек, который обновляет зависимости, меняет ключи, смотрит на красные сборки и знает, где лежит регламент отката. Это может быть сотрудник вашей команды после обучения или подрядчик на сопровождении. На странице инфраструктуры Hated.Ru сопровождение указано от 25 000 ₽ в месяц.
18Прозрачность
Что проверено в этой статье, а что нет
Все примеры кода запускались или проверялись инструментами в тестовой среде. Ниже честный список: где проверка была настоящей, а где она остановилась на разборе синтаксиса.
| Материал | Чем проверено | Чего не делалось |
|---|---|---|
| Конвейеры GitHub Actions и GitLab CI | Проверка по схемам файлов | Запуск на реальных сервисах |
| Скрипт выкладки и отката | ShellCheck, запуск в песочнице: восемь выкладок, откат, список релизов | Запуск на боевом сервере с nginx |
| Замер способов выкладки | Три прогона на 4000 файлов, скрипты приведены в статье | Проверка с PHP, базой данных и кэшем браузера |
| Dockerfile и Compose | Hadolint и команда docker compose config | Сборка образа и запуск контейнеров |
| Манифест Kubernetes | Проверка по схемам Kubernetes 1.37 | Применение в кластере |
| Terraform | Разбор парсером HCL | init, plan и создание ресурсов в облаке |
| Ansible | Проверка синтаксиса в ansible-core 2.19 | Запуск на реальной машине |
| Правила Prometheus | Разбор выражений парсером PromQL | Работа с реальными метриками и трафиком |
| Файлы .gitignore | Команда git check-ignore в тестовом репозитории | Проверка на вашем проекте |
| Скрипт DORA | Запуск на учебных данных | Проверка на журнале выкладок реального проекта |
| Факты, даты, версии | Сверены с источниками ниже 17 октября 2026 года | Сведения могут устареть: проверяйте их перед решением |
Цены Hated.Ru приведены со страниц услуг на ту же дату. Если вы заметите расхождение с актуальной ценой на сайте, ориентируйтесь на сайт.
19Источники
Источники
Страницы открыты и сверены 17 октября 2026 года.
- DORA: метрики эффективности поставки ПО, определения пяти метрик и их группировка.
- DORA: история метрик, 2014 год для первых четырёх и 2024 год для пятой.
- HPE Developer: начало DevOps, доклад Flickr на Velocity 2009 и конференция в Генте.
- Kubernetes: релизы и сроки поддержки, таблица версий на 17 октября 2026.
- Prometheus 3.0, анонс от 14 ноября 2024.
- Cisco DevNet: четыре золотых сигнала, задержка, трафик, ошибки, насыщение.
- Alertmanager и Telegram: руководство по подключению.
- Яндекс Облако: ошибка 403 при установке провайдеров Terraform.
- Яндекс Облако: начало работы с Terraform, файл
.terraformrcс зеркалом. - Яндекс Облако: блокировки состояния Terraform.
- Spacelift: смена лицензии Terraform, даты BUSL, OpenTofu 1.6.
- Хабр: OpenTofu закрыл реестр для России, новость от 28 августа 2024.
- Security Lab: Docker Hub заблокировал российских пользователей, 30 мая 2024.
- Хабр: зеркало Docker Hub в GitVerse, 3 июня 2024.
- Elastic: Elasticsearch снова открытый, 29 августа 2024.
- Хабр: настройка проекта на 1С-Битрикс в Git, практика команды, 2021 год.
- OPcache и выкладки через символические ссылки, описание проблемы и двух способов её решить.
- Защита GitHub Actions: минимальные права токена, закрепление действий по хешу, секреты.
- Страницы услуг Hated.Ru: внедрение CI/CD, инфраструктура, Яндекс Облако и VK Cloud.
Hated.Ru · DevOps
Покажем, какие ступени нужны вашему сайту
Пришлите адрес сайта и расскажите, как вы выкладываете обновления сейчас. Скажем, с какой ступени начать, сколько это займёт и сколько будет стоить, до начала работ. Работаем по всей России, офисы в Москве, Туле и Рязани.
Тула, Московское шоссе, 2в, офис 3. Рязань, 2-й Школьный переулок, 1. Москва, МКАД, 60-й километр, 4а.
