Bash-скрипты: когда рутина становится кодом
В продакшене Linux-системы Bash-скрипты — это не просто способ автоматизировать задачу, а способ устранить неопределенность. Представьте: вы развертываете сервис, где 50% времени уходит на ручные операции. Или у вас есть задача, которую выполняют три разных человека по-разному. Или вы хотите убедиться, что после обновления ядра все сервисы остались в живых. Вот тут и появляются скрипты.
Но проблема в том, что большинство скриптов пишутся в спешке, без учета будущего сопровождения. Они работают — пока не ломаются. А потом начинаются костыли, которые растут как грибы после дождя. В этой статье разберем, как писать скрипты, которые не ломаются, не тормозят и не превращаются в болото.
Проблема: почему скрипты становятся бомбой замедленного действия
Скрипты обычно пишутся для решения конкретной задачи. Например:
- Автоматизация деплоя.
- Мониторинг дискового пространства.
- Очистка логов.
- Настройка окружения перед релизом.
Но через год-два такой скрипт становится нечитаемым, медленным или неработоспособным. Почему?
- Отсутствие структуры: Все в одном файле, без разделения на функции.
- Жестко закодированные пути:
/home/user/logs/вместо$LOG_DIR. - Нет обработки ошибок: Если что-то ломается, скрипт молча падает.
- Зависимость от окружения: Работает у автора, но не в продакшене.
- Нет логов: Трудно отладить, если скрипт тихо умирает.
Результат: команда начинает бояться скриптов, а 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 |
Пример полного рабочего скрипта: мониторинг и очистка логов
Вот как может выглядеть реальный скрипт, который:
- Проверяет размер логов.
- Очищает старые файлы.
- Отправляет уведомление, если что-то пошло не так.
#!/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).
Вывод: как не превратить скрипты в технический долг
- Начинайте с правильной структуры: функции, переменные, модульность.
- Обрабатывайте ошибки:
set -euo pipefail+ явные проверки. - Используйте конфигурацию: не жёстко кодируйте пути и параметры.
- Логируйте всё: чтобы можно было отладить, если что-то пойдет не так.
- Тестируйте скрипты: запускайте их в тестовом окружении перед продакшеном.
- Храните в репозитории: чтобы была история изменений и возможность отката.
- Документируйте: даже короткий комментарий лучше, чем ничего.
Практический совет: Начните с маленького скрипта, который решает одну конкретную задачу. Затем постепенно улучшайте его:
- Добавьте обработку ошибок.
- Вынесите конфигурацию в отдельный файл.
- Разбейте на функции.
- Напишите тесты (да, даже для bash-скриптов можно писать тесты с помощью
bashateилиshellcheck).
Если вы будете следовать этим правилам, ваши скрипты не будут ломаться, не будут тормозить и не станут головной болью для команды.