Как написать скрипт развёртывания для Linux: 6 шагов с готовыми примерами | AdminWiki

Как написать скрипт развёртывания для Linux: 6 шагов с готовыми примерами

24 сентября 2026 13 мин. чтения

Скрипт развёртывания для Linux-сервера собирают за шесть этапов: постановка задачи, выбор инструмента, каркас с обработкой ошибок, параметризация аргументами и конфигами, разбиение на функции и тестирование в изолированной среде. В Bash каркас начинается с shebang и строки set -euo pipefail, а страховкой служат dry-run, shellcheck и автоматический откат, если проверка здоровья не прошла.

Ниже разбираем каждый этап на примерах для Ubuntu и Debian. Инструменты для сравнения: Bash, Python и Ansible. В финале даём готовый deploy.sh, который адаптируется под свой стек несколькими правками.

Ключевой риск ручного деплоя - тихий сбой: неудачный git pull, продолжение работы и выкатка старого кода. Обработку ошибок и откат закладывают в каркас сразу, а не добавляют после первого инцидента.

Что такое скрипт развёртывания и когда он нужен

Скрипт развёртывания - исполняемый файл, который выполняет фиксированную последовательность шагов по доставке и запуску приложения на сервере: обновляет код, ставит зависимости, прогоняет миграции, перезапускает сервисы и проверяет здоровье приложения. Один запуск вида ./deploy.sh --env prod --tag v1.4.2 заменяет десяток команд, которые обычно набирают руками по SSH.

Скрипт оправдан в таких случаях:

  • деплой повторяется по одному сценарию: от раза в неделю до нескольких раз в день;
  • серверов немного, ориентировочно от 1 до 10, и каждый обслуживается вручную или через SSH;
  • полноценного CI/CD нет, либо пайплайн собирает артефакт, но не умеет доставлять его на хост;
  • нужен запуск по требованию: хотфикс ночью, повторный деплой после отката, проверка окружения staging.

Скрипт не нужен, когда конфигурацию меняют один раз при настройке сервера, когда деплой полностью описан манифестами Kubernetes или когда уже работает GitLab CI с раннером на целевом хосте. В этих сценариях самописный сценарий создаст второй источник правды и рано или поздно разойдётся с основным процессом.

Чем скрипт деплоя отличается от CI/CD-пайплайна

CI/CD оркестрирует сборку, тесты и доставку артефакта. Скрипт развёртывания - один из исполняемых шагов этого процесса либо самостоятельный инструмент там, где пайплайна нет. Оба варианта совместимы, и чаще всего скрипт вызывается последним шагом пайплайна по SSH.

Пример связки: GitLab CI собирает Docker-образ, тегирует его коммитом и пушит в registry. На сервере остаётся скрипт, который делает docker compose pull, docker compose up -d и проверяет healthcheck. Общую картину подходов, включая сравнение shell-скриптов с платформами деплоя и оркестраторами, разбираем в материале Системы развертывания приложений: сравнение подходов и выбор инструмента.

Практическая граница проста: если логика доставки укладывается в 100-200 строк и меняется редко, скрипт дешевле пайплайна. Если шагов больше, а участников несколько, переносите логику в CI или Ansible.

Шаг 1. Постановка задачи: что должен делать скрипт

Требования фиксируют до первой строки кода, иначе сценарий придётся переписывать после каждого нового окружения. Минимальный чек-лист выглядит так:

  1. Что деплоим: исходный код, собранный артефакт, Docker-образ, набор статических файлов.
  2. Куда: хост, целевой каталог, пользователь, от имени которого работать.
  3. Какие зависимости нужны: системные пакеты, Docker, Python venv, Node.js.
  4. Есть ли миграции БД и в каком порядке они идут относительно перезапуска сервиса.
  5. Какие сервисы перезапускать: systemd unit, docker compose, supervisor.
  6. Как проверять успех: curl healthcheck, systemctl is-active, код возврата миграций.
  7. Что делать при ошибке: откат на предыдущий релиз, уведомление в чат, остановка без изменений.

Заполненный чек-лист для типового веб-приложения: репозиторий git@github.com:example/app.git, ветка или тег main, каталог релизов /var/www/app/releases, сервис app.service, конфиг /etc/app/prod.env, healthcheck http://127.0.0.1:8080/health, откат на предыдущий симлинк релиза. Без этого списка скрипт превращается в набор случайных команд, которые работают только на машине автора.

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

Шаг 2. Выбор инструмента: Bash, Python или Ansible

Выбор зависит от масштаба и требуемой идемпотентности. Идемпотентность означает, что повторный запуск не меняет результат и не ломает систему: скрипт проверяет состояние перед действием.

КритерийBashPythonAnsible
Порог входаМинимальный, всё есть на хостеСредний, нужен интерпретатор и venvСредний, нужен control node и SSH-доступ
ИдемпотентностьРеализуете сами проверкамиРеализуете сами, помогают библиотекиВстроена в модули
СекретыEnv-файл с правами 600Переменные окружения, keyring, внешние хранилищаAnsible Vault и внешние lookups
Отладкаbash -x, shellcheckpdb, traceback, pytestФлаги verbosity, режим --check
Масштаб1-5 серверов1-20 хостов со сложной логикойОт 5 хостов и выше
Агент на хостеНе нуженНужен PythonНе нужен, только SSH

Конкретные сценарии: деплой статики на один Nginx закрывается Bash за 40 строк. Вызов API мониторинга, парсинг JSON и миграции с ветвлениями удобнее описать на Python. Парк из 20 серверов с ролями и общими переменными переводите на Ansible, где роль описывает состояние, а не последовательность команд. Готовый пример такого подхода с Jinja2-шаблонами и проверкой результата есть в статье Автоматизированная настройка и проверка кеша Nginx: готовые Ansible-плейбуки и тесты нагрузки.

В этой статье основной пример написан на Bash: он ближе всего к формату «с нуля» и не требует установки дополнительных пакетов. Логика шагов переносится на Python и Ansible без изменений.

Шаг 3. Каркас Bash-скрипта: shebang, set -euo pipefail и логирование

Каркас занимает десяток строк и определяет поведение всего сценария. Сохраните файл как deploy.sh, добавьте права на запуск командой chmod +x deploy.sh и начните с такого блока:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

trap 'echo "Ошибка на строке $LINENO, код $?" >&2' ERR

log() {
  local level="$1"; shift
  printf '%s [%s] %s\n' "$(date -Is)" "$level" "$*" >&2
}

Разбор строки set -euo pipefail по флагам:

  • -e заставляет bash немедленно завершиться, если команда возвращает ненулевой статус выхода. Без этого падающая команда не останавливает скрипт: выполнение продолжается со следующей строки, и если последняя строка успешна, весь скрипт получает успешный код выхода - ошибку легко пропустить. Если сбой команды ожидаем и обрабатывается вручную, её статус выхода можно явно игнорировать;
  • -u делает обращение к неопределённой переменной ошибкой: программа немедленно завершается с кодом выхода 1 и сообщением вида "firstname: unbound variable" в stderr, что ловит опечатки вроде $TAGG вместо $TAG;
  • -o pipefail предотвращает маскировку ошибок в конвейере: если любая команда в конвейере падает, её код возврата используется как код возврата всего конвейера. По умолчанию код возврата конвейера - это код последней команды, даже если она успешна;
  • IFS=$'\n\t' защищает пути с пробелами от разбиения на слова.

Без set -euo pipefail сценарий продолжит работу после неудачного git pull и задеплоит старый код: команда завершилась с ненулевым кодом, скрипт пошёл дальше, симлинк переключился на прежний релиз. Это классическая причина инцидентов при ночных выкатках. Поведение флагов -e, -u и -o pipefail подробно разобрано в Bash Strict Mode: set -euo pipefail Explained и в set -e, -u, -o, -x pipefail explanation.

Функция log() пишет в stderr, чтобы не смешивать служебные сообщения с полезным выводом команд, и добавляет метку времени в формате ISO. Линтер shellcheck держите в цикле проверки: команда shellcheck deploy.sh находит ошибки quoting, ссылки на неопределённые переменные и устаревший синтаксис, которые вызывают сбои shell-скриптов в продакшене. В частности, предупреждение SC2086 указывает на неэкранированные переменные: они подвержены разбиению на слова и раскрытию шаблонов (globbing), из-за чего скрипты ломаются на именах файлов с пробелами или glob-символами, а SC2046 срабатывает, когда подстановка команды не заключена в кавычки. Разбор SC2086 приведён в ShellCheck: SC2086 - Double quote to prevent globbing and word splitting, а обзор возможностей линтера - в ShellCheck: Catch Shell Script Bugs Before They Reach Production. ShellCheck упакован в большинстве Linux-дистрибутивов и ставится, например, через apt-get install shellcheck или dnf install ShellCheck.

Шаг 4. Аргументы командной строки и конфигурационные файлы

Скрипт должен работать в staging и prod без правки кода. Для коротких флагов используйте getopts, для длинных, как в примере ниже, обычный разбор while с case.

usage() {
  echo "Использование: $0 --env <staging|prod> --tag <версия> [--dry-run]" >&2
  exit 1
}

ENV_NAME=""; TAG=""; DRY_RUN=0
while [ $# -gt 0 ]; do
  case "$1" in
    --env) ENV_NAME="$2"; shift 2 ;;
    --tag) TAG="$2"; shift 2 ;;
    --dry-run) DRY_RUN=1; shift ;;
    -h|--help) usage ;;
    *) echo "Неизвестный флаг: $1" >&2; usage ;;
  esac
done
[ -n "$ENV_NAME" ] && [ -n "$TAG" ] || usage

Валидация обязательна: неизвестный флаг завершает работу с кодом 1 и подсказкой usage(), иначе опечатка вроде --tags v1.4.2 приведёт к деплою пустой версии.

Конфигурацию храните отдельно от кода, в двух форматах. Простой env-файл формата KEY=VALUE подключается так:

set -a
source "/etc/app/${ENV_NAME}.env"
set +a

Конструкция set -a помечает все присвоенные переменные как экспортируемые, а set +a снимает этот режим. Для сложных структур (списки хостов, вложенные настройки) берите YAML или INI и разбирайте их через python3 -c либо отдельную утилиту.

Правила работы с секретами жёсткие: env-файл добавляйте в .gitignore, выставляйте права chmod 600, храните вне каталога репозитория. Пароли в аргументах командной строки недопустимы: аргументы процесса видны другим пользователям хоста через список процессов. Автоматизацию регулярных проверок доступа и утечек описываем в статье Автоматизация аудита безопасности: инструменты и скрипты для регулярных проверок. Рабочий вызов скрипта после параметризации выглядит так: ./deploy.sh --env prod --tag v1.4.2 --dry-run.

Шаг 5. Разбиение скрипта на функции и модули

Монолитный файл на 300 строк невозможно ревьюить и опасно править. Разбейте логику на функции, каждая из которых делает одно действие и возвращает код выхода: check_deps(), load_env(), fetch_code(), install_deps(), run_migrations(), restart_service(), healthcheck(), rollback().

Общие функции выносите в библиотеку и подключайте через source. Структура каталогов проекта:

deploy.sh
lib/common.sh
lib/healthcheck.sh
conf/prod.env
conf/staging.env

Подключение выглядит так:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
# shellcheck source=lib/healthcheck.sh
source "${SCRIPT_DIR}/lib/healthcheck.sh"

Вычисление SCRIPT_DIR через BASH_SOURCE делает пути независимыми от текущего каталога запуска. Функция main() в конце файла вызывает шаги последовательно, и сценарий читается сверху вниз без прыжков. Для Ansible аналог разбиения - роли и tasks внутри них, для Python - модули и пакеты.

Шаг 6. Обработка ошибок и откат изменений

Защита строится на трёх уровнях. Превентивный уровень проверяет зависимости, права и свободное место до первого изменения. Реактивный ловит сбой через trap на ERR, коды возврата и логирование. Откатный возвращает систему в предыдущее состояние, когда проверка здоровья не прошла.

Рабочий шаблон отката опирается на симлинк current и каталог releases. Перед деплоем сохраняется путь предыдущего релиза, после переключения запускается healthcheck с тремя попытками по 5 секунд. Если проверка не проходит, симлинк возвращается назад, сервис перезапускается, скрипт завершается с кодом 1:

PREV_RELEASE="$(readlink -f "$CURRENT_LINK" 2>/dev/null || true)"
trap rollback ERR

healthcheck() {
  local i
  for i in 1 2 3; do
    curl -fsS --max-time 5 "$HEALTH_URL" >/dev/null && return 0
    sleep 5
  done
  return 1
}

rollback() {
  trap - ERR
  log WARN "Healthcheck не прошёл, откат на ${PREV_RELEASE:-предыдущий релиз отсутствует}"
  if [ -n "$PREV_RELEASE" ]; then
    ln -sfn "$PREV_RELEASE" "$CURRENT_LINK"
    systemctl restart "$SERVICE"
  fi
  exit 1
}

Разница в трудозатратах заметна сразу: ручной откат занимает минуты поиска предыдущего тега и повторной выкатки, автоматический - секунды. Сценарии отказоустойчивости, включая canary, blue-green и rolling deployment с готовыми командами отката, разобраны в статье Обновления и отказоустойчивость: мифы и реальные сценарии.

Миграции БД требуют отдельного плана: down-миграции пишут не всегда, поэтому перед деплоем схемы делайте дамп или снапшот тома. Откат кода без отката схемы часто оставляет приложение в состоянии, когда оно не стартует.

Безопасная проверка скрипта перед запуском на продакшене

Протокол тестирования идёт от самого дешёвого шага к самому дорогому:

  1. Проверка синтаксиса: bash -n deploy.sh и линтер shellcheck deploy.sh.
  2. Запуск в контейнере с тем же дистрибутивом: docker run --rm -it -v "$PWD:/work" ubuntu:24.04 bash, дальше внутри монтируемого каталога.
  3. Прогон на виртуальной машине или staging-сервере с копией конфигов и тестовой базой.
  4. Режим --dry-run, который печатает команды без выполнения: ловит ошибки в путях, переменных и подстановках.
  5. Проверка идемпотентности: запустите скрипт дважды подряд, второй запуск не должен ломать состояние.
  6. Проверка отката: намеренно сломайте healthcheck (остановите сервис или подмените URL) и убедитесь, что rollback вернул предыдущий релиз.

Чек-лист перед запуском на продакшене:

  1. Свежий бэкап базы данных и каталога конфигов.
  2. Согласованное окно деплоя и уведомление команды.
  3. Проверенный план отката с указанием команды и ответственного.
  4. Доступ к логам: journalctl -u app.service -f и логи приложения.
  5. Права пользователя деплоя ограничены каталогом приложения, без полного sudo.
  6. Секреты не попадают в stdout и в файлы логов скрипта.
  7. Мониторинг и алерты включены до начала выкатки.
  8. Лимит времени на деплой, после которого срабатывает ручной откат.
  9. Проверка свободного места на диске: df -h для каталога релизов.
  10. Договорённость о том, кто принимает решение об откате.

Изменения в продакшене без ревью и без прогона на копии окружения регулярно приводят к простоям. Разбор четырёх типовых провалов с конкретными шагами предотвращения собран в статье Ошибки системных администраторов: 4 главных провала и способы их предотвращения.

Режим dry-run удобно реализовать через обёртку run(), которая либо печатает команду, либо выполняет её:

run() {
  if [ "$DRY_RUN" -eq 1 ]; then
    log INFO "DRY-RUN: $*"
  else
    "$@"
  fi
}

Готовый пример скрипта развёртывания и адаптация под свой стек

Ниже собран целиком deploy.sh: shebang, set -euo pipefail, разбор аргументов, функции, healthcheck, откат и поддержка dry-run. Каталог релизов /var/www/app/releases и симлинк /var/www/app/current создайте заранее.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

APP_NAME="app"
BASE_DIR="/var/www/${APP_NAME}"
RELEASES_DIR="${BASE_DIR}/releases"
CURRENT_LINK="${BASE_DIR}/current"
SERVICE="${APP_NAME}.service"
GIT_URL="git@github.com:example/app.git"
HEALTH_URL="http://127.0.0.1:8080/health"

ENV_NAME=""
TAG=""
DRY_RUN=0
PREV_RELEASE=""

log() {
  local level="$1"; shift
  printf '%s [%s] %s\n' "$(date -Is)" "$level" "$*" >&2
}

usage() {
  echo "Использование: $0 --env <staging|prod> --tag <версия> [--dry-run]" >&2
  exit 1
}

run() {
  if [ "$DRY_RUN" -eq 1 ]; then
    log INFO "DRY-RUN: $*"
  else
    "$@"
  fi
}

parse_args() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --env) ENV_NAME="$2"; shift 2 ;;
      --tag) TAG="$2"; shift 2 ;;
      --dry-run) DRY_RUN=1; shift ;;
      -h|--help) usage ;;
      *) log ERROR "Неизвестный флаг: $1"; usage ;;
    esac
  done
  [ -n "$ENV_NAME" ] && [ -n "$TAG" ] || usage
}

check_deps() {
  local bin
  for bin in git curl systemctl readlink; do
    command -v "$bin" >/dev/null || { log ERROR "не найдена утилита $bin"; return 1; }
  done
  [ -d "$RELEASES_DIR" ] || { log ERROR "нет каталога $RELEASES_DIR"; return 1; }
}

load_env() {
  local file="/etc/${APP_NAME}/${ENV_NAME}.env"
  [ -r "$file" ] || { log ERROR "нет конфига $file"; return 1; }
  set -a; source "$file"; set +a
}

fetch_code() {
  run git clone --depth 1 --branch "$TAG" "$GIT_URL" "${RELEASES_DIR}/${TAG}"
}

healthcheck() {
  local i
  for i in 1 2 3; do
    curl -fsS --max-time 5 "$HEALTH_URL" >/dev/null && return 0
    sleep 5
  done
  return 1
}

rollback() {
  trap - ERR
  log WARN "Healthcheck не прошёл, откат на ${PREV_RELEASE:-предыдущий релиз отсутствует}"
  if [ -n "$PREV_RELEASE" ]; then
    ln -sfn "$PREV_RELEASE" "$CURRENT_LINK"
    systemctl restart "$SERVICE"
  fi
  exit 1
}

main() {
  parse_args "$@"
  check_deps
  load_env
  PREV_RELEASE="$(readlink -f "$CURRENT_LINK" 2>/dev/null || true)"
  log INFO "Деплой $TAG в $ENV_NAME, предыдущий релиз: ${PREV_RELEASE:-нет}"
  trap rollback ERR
  fetch_code
  run ln -sfn "${RELEASES_DIR}/${TAG}" "$CURRENT_LINK"
  run systemctl restart "$SERVICE"
  if [ "$DRY_RUN" -eq 1 ]; then
    log INFO "DRY-RUN завершён, изменения не применялись"
    exit 0
  fi
  healthcheck
  log INFO "Релиз $TAG активен"
}

main "$@"

Запуск и проверка результата:

chmod +x deploy.sh
./deploy.sh --env prod --tag v1.4.2 --dry-run
./deploy.sh --env prod --tag v1.4.2
curl -fsS http://127.0.0.1:8080/health
systemctl is-active app.service
readlink -f /var/www/app/current

Адаптация под другой стек сводится к замене шага доставки и команды проверки:

Что деплоимШаги вместо git clone и restartПроверка
Python-приложениеsource .venv/bin/activate, pip install -r requirements.txt, alembic upgrade head, systemctl restart app.servicecurl -fsS http://127.0.0.1:8080/health, systemctl is-active app.service
Docker Composedocker compose pull, docker compose up -d, docker image prune -fdocker compose ps, docker compose logs --tail=50
Статика на Nginxrsync -a --delete ./dist/ /var/www/site/, nginx -t, systemctl reload nginxcurl -I http://127.0.0.1/, проверка cache-control у ассетов

Команды выше опираются на утилиты coreutils, curl, git и systemd. Набор базовых пакетов и версия Bash отличаются между дистрибутивами и релизами, поэтому перед переносом скрипта на другой хост проверьте наличие нужных утилит (например, через command -v, как в check_deps), имена пакетов и путей к unit-файлам, а также версию Bash: синтаксис с pipefail требует 4.x и выше, для массивов и ${BASH_SOURCE[0]} удобнее 5.x.

Дальше остаётся один шаг, который экономит больше всего времени: зафиксируйте в репозитории не только deploy.sh, но и conf/prod.env.example, и добавьте вызов shellcheck в pre-commit hook. Тогда ошибки в путях и переменных будут всплывать до выкатки, а не в момент простоя.

Поделиться:
Сохранить гайд? В закладки браузера