Bash-скрипты: когда рутина становится кодом

В продакшене Linux-системы Bash-скрипты — это не просто способ автоматизировать задачу, а способ устранить неопределенность. Представьте: вы развертываете сервис, где 50% времени уходит на ручные операции. Или у вас есть задача, которую выполняют три разных человека по-разному. Или вы хотите убедиться, что после обновления ядра все сервисы остались в живых. Вот тут и появляются скрипты.

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


Проблема: почему скрипты становятся бомбой замедленного действия

Скрипты обычно пишутся для решения конкретной задачи. Например:

  • Автоматизация деплоя.
  • Мониторинг дискового пространства.
  • Очистка логов.
  • Настройка окружения перед релизом.

Но через год-два такой скрипт становится нечитаемым, медленным или неработоспособным. Почему?

  1. Отсутствие структуры: Все в одном файле, без разделения на функции.
  2. Жестко закодированные пути: /home/user/logs/ вместо $LOG_DIR.
  3. Нет обработки ошибок: Если что-то ломается, скрипт молча падает.
  4. Зависимость от окружения: Работает у автора, но не в продакшене.
  5. Нет логов: Трудно отладить, если скрипт тихо умирает.

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


Практика: как писать скрипты, которые работают

1. Начинаем с правильной структуры

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

Пример структуры:

#!/bin/bash
set -euo pipefail  # Жесткий режим: ошибки не игнорируются

# --- Настройки ---
LOG_DIR="/var/log/myapp"
BACKUP_DIR="/backups"
MAX_LOG_SIZE=100M

# --- Функции ---
clean_logs() {
    local dir="$1"
    find "$dir" -type f -size +${MAX_LOG_SIZE} -exec rm -f {} \;
}

backup_logs() {
    local src="$1"
    local dest="$2"
    tar -czf "${dest}/logs-$(date +%Y%m%d).tar.gz" -C "$src" .
}

# --- Основной поток ---
clean_logs "$LOG_DIR"
backup_logs "$LOG_DIR" "$BACKUP_DIR"
echo "Логи очищены и забэкаплены."

Почему так работает:

  • set -euo pipefail — убивает скрипт при первой ошибке, не давая продолжить с мусорными данными.
  • Переменные в начале — легко менять конфигурацию без поиска по коду.
  • Функции — можно переиспользовать логику.

2. Обработка ошибок: не молчите, кричите

Самая частая ошибка — игнорировать ошибки. Если скрипт падает, нужно знать почему.

Пример с обработкой ошибок:

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/deploy.log"

deploy_app() {
    if ! git pull origin main; then
        echo "$(date) | Ошибка при pull из репозитория" >> "$LOG_FILE"
        exit 1
    fi

    if ! docker-compose up -d; then
        echo "$(date) | Ошибка при старте контейнеров" >> "$LOG_FILE"
        exit 1
    fi

    echo "$(date) | Развертывание успешно" >> "$LOG_FILE"
}

deploy_app

Что здесь важно:

  • Логи записываются даже при ошибке.
  • exit 1 — сигнал, что что-то пошло не так.
  • Если скрипт падает, CI/CD или мониторинг сразу увидит проблему.

3. Переменные окружения и конфигурация

Никогда не жёстко не прописывайте пути или параметры. Используйте:

  • Переменные окружения ($LOG_DIR).
  • Конфиг-файлы (YAML, JSON, или даже .env).
  • Аргументы командной строки ($1, $2).

Пример с конфигом:

#!/bin/bash
set -euo pipefail

# Загружаем конфиг из файла
CONFIG_FILE="/etc/myapp/config.yml"
if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "Конфигурационный файл не найден: $CONFIG_FILE"
    exit 1
fi

# Используем yq для парсинга YAML (нужно установить: https://github.com/mikefarah/yq)
LOG_DIR=$(yq eval '.log_dir' "$CONFIG_FILE")
BACKUP_DIR=$(yq eval '.backup_dir' "$CONFIG_FILE")

echo "Логи будут храниться в: $LOG_DIR"
echo "Бэкапы в: $BACKUP_DIR"

Почему это лучше:

  • Конфиг отделен от логики.
  • Можно менять настройки без изменения кода.
  • Легче тестировать разные окружения.

4. Логирование: как не потеряться в потоке вывода

Логи должны быть структурированными и полезными.

Пример логгера:

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/monitor.log"

log() {
    local level="$1"
    local message="$2"
    echo "$(date '+%Y-%m-%d %H:%M:%S') | [$level] $message" >> "$LOG_FILE"
}

monitor_disk() {
    local threshold=90
    local usage=$(df / --output=pcent | tail -n1 | tr -d '%')

    if [[ "$usage" -gt "$threshold" ]]; then
        log "WARNING" "Дисковое пространство: $usage% (превышает порог $threshold%)"
        exit 1
    else
        log "INFO" "Дисковое пространство в норме: $usage%"
    fi
}

monitor_disk

Что здесь работает:

  • Уровни логирования (INFO, WARNING, ERROR).
  • Временные метки для отслеживания последовательности событий.
  • Сохранение в файл для анализа.

Типичные ошибки и как их избежать

Ошибка Пример Как исправить
Жестко закодированные пути rm -rf /var/log/app/*.log Использовать переменные: LOG_DIR="/var/log/app"
Нет обработки ошибок Скрипт молча падает при ошибке set -euo pipefail + явные проверки
Слишком много логики в одном файле 500 строк без функций Разбивать на функции/отдельные скрипты
Зависимость от внешних утилит awk '{print $1}' file.txt без проверки Проверять наличие утилит: command -v awk >/dev/null
Нет версионирования Скрипт лежит в /usr/local/bin без гит-репозитория Хранить в репозитории, делать коммиты
Плохое именование script.sh вместо deploy_app.sh Ясные имена: backup_logs.sh, check_disk.sh

Пример полного рабочего скрипта: мониторинг и очистка логов

Вот как может выглядеть реальный скрипт, который:

  1. Проверяет размер логов.
  2. Очищает старые файлы.
  3. Отправляет уведомление, если что-то пошло не так.
#!/bin/bash
set -euo pipefail

# --- Конфигурация ---
LOG_DIR="/var/log/myapp"
MAX_LOG_SIZE=100M
MAX_LOG_AGE=30d
ADMIN_EMAIL="admin@example.com"

# --- Функции ---
clean_old_logs() {
    local dir="$1"
    local age="$2"
    find "$dir" -type f -mtime +${age%?} -delete
}

check_log_size() {
    local dir="$1"
    local size="$2"
    local big_logs=$(find "$dir" -type f -size +${size} -printf "%p %s\n" | sort -k2 -nr)

    if [[ -n "$big_logs" ]]; then
        echo "⚠️ Большие логи найдены:"
        echo "$big_logs" | head -n 5
        return 1
    fi
    return 0
}

send_alert() {
    local subject="⚠️ Проблема с логами на $(hostname)"
    local message="Скрипт очистки логов завершился с ошибками."
    echo "$message" | mail -s "$subject" "$ADMIN_EMAIL"
}

# --- Основной поток ---
echo "$(date) | Начало очистки логов"

if ! check_log_size "$LOG_DIR" "$MAX_LOG_SIZE"; then
    send_alert
    exit 1
fi

clean_old_logs "$LOG_DIR" "$MAX_LOG_AGE"
echo "$(date) | Логи очищены. Старые файлы удалены."

Что здесь важно:

  • Проверка размера логов перед очисткой.
  • Уведомление администратора при проблемах.
  • Логирование каждого шага.
  • Параметризация (можно легко поменять MAX_LOG_SIZE).

Вывод: как не превратить скрипты в технический долг

  1. Начинайте с правильной структуры: функции, переменные, модульность.
  2. Обрабатывайте ошибки: set -euo pipefail + явные проверки.
  3. Используйте конфигурацию: не жёстко кодируйте пути и параметры.
  4. Логируйте всё: чтобы можно было отладить, если что-то пойдет не так.
  5. Тестируйте скрипты: запускайте их в тестовом окружении перед продакшеном.
  6. Храните в репозитории: чтобы была история изменений и возможность отката.
  7. Документируйте: даже короткий комментарий лучше, чем ничего.

Практический совет: Начните с маленького скрипта, который решает одну конкретную задачу. Затем постепенно улучшайте его:

  • Добавьте обработку ошибок.
  • Вынесите конфигурацию в отдельный файл.
  • Разбейте на функции.
  • Напишите тесты (да, даже для bash-скриптов можно писать тесты с помощью bashate или shellcheck).

Если вы будете следовать этим правилам, ваши скрипты не будут ломаться, не будут тормозить и не станут головной болью для команды.