Хочу поделиться с сообществом простым и полезным шаблоном скрипта-обёртки на bash для запуска заданий по cron (а сейчас и systemd timers), ��оторый моя команда повсеместно использует много лет.
Сначала пара слов о том зачем это нужно, какие проблемы решает. С самого начала моей работы системным администратором linux, я обнаружил, что cron не очень удобный планировщик задач. При этом практически безальтернативный. Чем больше становился мой парк серверов и виртуальных машин, тем больше я получал абсолютно бесполезных почтовых сообщений "From: Cron Daemon". Задание завершилось с ошибкой - cron напишет об этом. Задание выполнено успешно, но напечатало что-нибудь в STDOUT/STDERR - cron всё равно напишет об этом. При этом даже нельзя отформатировать тему почтового сообщения для удобной автосортировки. Сначала были годы борьбы с использованием разных вариаций из > /dev/null, 2> /dev/null, > /dev/null 2>&1, | mail -E -s '<Subject>' root@. Потом я нашёл Cronic - обёртку на bash, которая скрывает вывод запускаемой задачи, если она завершена успешно. Стало полегче, но обнаружилось, что от некоторых заданий всё же лучше получать сообщение "Task OK", чтобы не столкнуться в самый неподходящий момент с тем, что выполнение задания тихо сломано месяц назад. Постепенно копились и другие хотелки:
иногда требуется, чтобы задание было автоматически принудительно остановлено, если выполняется больше определённого времени;
иногда нужны гарантии, что в каждый момент времени запущена только одна копия задания;
бывает так, что запускать задачу нужно с рандомной задержкой по времени (такая дисперсия иногда нужна, чтобы не положить какой-нибудь сервис одновременными запросами с большого количества машин).
В какой-то момент я отложил в сторону cronic и написал свой шаблон для запуска периодических заданий, в котором реализовано всё, что перечислено выше. Вот что он умеет:
сохраняет в лог-файлы STDERR и STDOUT выполняемых команд;
если задание завершилось ошибкой, то отправляет на заданный электронный адрес последние 10000 (можно настроить любое) строк STDOUT и STDERR;
опционально может отправить метрику в Zabbix, если задача выполнена успешно (удобно для сброса времени срабатывания триггера);
гарантирует, что одновременно будет запущена только одна копия задания;
опционально может запускать задачу с рандомной (в заданном диапазоне) задержкой по времени;
опционально можно установить максимальное время работы, по истечению которого задача будет принудительно завершена с ошибкой.

Давайте посмотрим на сам шаблон
#!/usr/bin/env bash ## # bash options ## set -eu -o pipefail export LC_ALL="C" export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ## # Variables ## SET_EXECUTION_LOCK="${SET_EXECUTION_LOCK:-}" MAX_EXECUTION_TIME="${MAX_EXECUTION_TIME:-10}" START_DELAY_RANGE="${START_DELAY_RANGE:-0-0}" LOGS="/var/log/$(basename "$0")" HOSTNAME="${HOSTNAME:-$(hostname -f)}" REPORT_MAIL="monitoring@example.com" REPORT_SUBJ="$0 fail on $HOSTNAME" #ZABBIX_ITEM="example.task.ok" ## # Execution lock and timeout ## if [[ -n "$MAX_EXECUTION_TIME" ]]; then command="timeout -v -k 60 $MAX_EXECUTION_TIME" fi if [[ -n "$SET_EXECUTION_LOCK" ]]; then command="flock -E 0 -n $0 ${command:-}" fi if [[ -z "${_run_:-}" ]]; then sleep "$(shuf -i "$START_DELAY_RANGE" -n 1)" export _run_=1 exec ${command:-} "$0" "$@" fi ## # Functions ## print_logs() { cd "$LOGS" echo "Trace of $HOSTNAME:$0" for log in stderr stdout; do if [[ -s "$log" ]]; then echo "----- $(basename "$log")" tail -n 10000 "$log" fi done } send_to_zabbix() { local item="${1:-}" local value="${2:-1}" zabbix_sender -c /etc/zabbix/zabbix_agentd.conf -s "$HOSTNAME" -k "$item" -o "$value" } on_exit() { return } on_error() { print_logs 2>&1 | mail -E -s "$REPORT_SUBJ" "$REPORT_MAIL" on_exit # Нам не нужно лишнее письмо от crond exit 0 } main() { set -x "$@" if [[ -n "${ZABBIX_ITEM:-}" ]]; then send_to_zabbix "${ZABBIX_ITEM:-}" 1 fi } ## # Main ## trap on_error ERR trap on_exit EXIT [[ -d "$LOGS" ]] || mkdir -p "$LOGS" (main "$@" > "$LOGS/stdout" 2> "$LOGS/stderr")
В целом скрипт тривиален, но некоторые пояснения, думаю, требуются.
Переменные которые определяют параметры запуска. Их значения могут быть установлены напрямую или через одноимённые переменные окружения.
# Максимальное время после которого задача будет принудительно завершена. MAX_EXECUTION_TIME="${MAX_EXECUTION_TIME:-8h}" # Не пустое значение запрещает запуск нескольких копий задачи. SET_EXECUTION_LOCK="${SET_EXECUTION_LOCK:-}" # Случайное число из этого диапазона определяет количество секунд задержки # перед стартом. START_DELAY_RANGE="${START_DELAY_RANGE:-0-0}"
Точка входа, запускаем главную функцию в subshell. Это нужно для того, чтобы в случае ошибки не была завершена сама обёртка.
(main "$@" > "$LOGS/stdout" 2> "$LOGS/stderr")
Основная функция.
main() { # Включаем вывод в STDERR выполняемых команд с аргументами set -x # Здесь помещаем вызов или логику задачи на bash. Если использовать # "$@", то здесь будет выполнена команда переданная обёртке в качестве # аргумента командной строки "$@" # Если определена переменная ZABBIX_ITEM, то отправляем метрику в сервер Zabbix if [[ -n "${ZABBIX_ITEM:-}" ]]; then send_to_zabbix "${ZABBIX_ITEM:-}" 1 fi }
on_error() - функция, которая будет вызвана в случае ошибки.
on_error() { # Отправляем трейсы print_logs 2>&1 | mail -E -s "$REPORT_SUBJ" "$REPORT_MAIL" on_exit # Завершаем работу с rc=0, нам не нужно лишнее письмо от crond exit 0 }
Функция on_exit() в шаблоне пуста. В неё можно добавить команды, которые будут выполнены перед завершением скрипта. Например, команды очистки временных файлов.
Что можно улучшить
Если вы используете Sentry для трекинга ошибок, то при помощи sentry-cli можно заменить отправку трейсов по электронной почте на отправку их в Sentry.
Можно отправлять метрики успешного завершения задачи в Prometheus/VictoriaMetrics, при помощи curl (нужен pushgateway) или, что проще, использовать prometheus node_exporter textfile collector.
