Перейти к содержимому

Настройка Cron: практический разбор

Cron — стандартный планировщик в Linux, который встречается в каждой инфраструктуре. Задачи сыплются, логи копятся, окружение подводит. Разберём, как настраивать cron надёжно и где он не подходит.

Когда cron нужен, а когда нет

Cron подходит для простых периодических задач: бэкапы, ротация логов, чистка временных файлов, периодические уведомления. Это демон, который спит между запусками — никаких ресурсов не ест.

Не используйте cron для:

  • задач, требующих миллисекундной точности (cron запускает с точностью до минуты)
  • задач с жёсткими зависимостями от других сервисов (systemd-юниты с After=)
  • долгих процессов, которые могут наложиться друг на друга (нужен lock)

Anatomy of a crontab entry

Формат строки:

* * * * * command
- - - - -
| | | | |
| | | | └── день недели (0-7, 0 и 7 = воскресенье)
| | | └──── месяц (1-12)
| | └────── день месяца (1-31)
| └──────── час (0-23)
└────────── минута (0-59)

Специальные значения:

@reboot   — при загрузке
@yearly   — раз в год (0 0 1 1 *)
@monthly  — раз в месяц (0 0 1 * *)
@weekly   — раз в неделю (0 0 * * 0)
@daily    — раз в день (0 0 * * *)
@hourly   — раз в час (0 * * * *)

Примеры:

0 3 * * *          /opt/scripts/backup.sh       # каждый день в 03:00
15,45 * * * *      /usr/local/bin/check.sh      # каждые 15 и 45 минут
0 */4 * * *        /opt/metrics/collect.sh       # каждые 4 часа
0 9-17 * * 1-5     /opt/reports/daily.sh         # каждый час в рабочее время будней

crontab -e и crontab -l: типовые команды

crontab -l              # показать текущий crontab пользователя
crontab -e              # редактировать crontab (откроется в EDITOR)
crontab -r              # удалить crontab (без подтверждения!)
crontab -l -u username  # посмотреть crontab другого пользователя (от root)
crontab filename        # загрузить задачи из файла
Примечание

По умолчанию редактор — vi. Изменить: export EDITOR=nano.

Где лежат системные расписания

Кроме пользовательских crontab, есть системные файлы:

/etc/crontab                # системный crontab (формат отличается — есть поле пользователя)
/etc/cron.d/                # каталог для drop-in файлов
/etc/cron.daily/            # ежедневные задачи (run-parts)
/etc/cron.hourly/           # ежечасные задачи
/etc/cron.monthly/          # ежемесячные задачи
/etc/cron.weekly/           # еженедельные задачи
/var/spool/cron/crontabs/   # пользовательские crontab-файлы

Строки в /etc/crontab и /etc/cron.d/* содержат поле username:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root

* * * * * root /opt/scripts/check.sh
Предупреждение

Не добавляйте задачи напрямую в /etc/crontab. Используйте /etc/cron.d/. Это безопаснее при обновлении пакета cron.

Окружение и PATH: почему задачи ломаются в cron

Cron запускает команды с минимальным окружением. Типичная ошибка:

# в терминале работает
/opt/scripts/backup.sh

# в cron — "command not found"

Причина: в cron переменная PATH содержит только /usr/bin:/bin. Решения:

Явно указывайте полные пути:

0 3 * * * /usr/bin/python3 /opt/scripts/backup.py

Задавайте PATH в crontab:

PATH=/usr/local/bin:/usr/bin:/bin:/opt/scripts
0 3 * * * backup.sh

Используйте обёртку-скрипт:

#!/bin/bash
# /opt/scripts/run_backup.sh
source /etc/profile
cd /opt/project || exit 1
./backup.sh
Подсказка

Всегда проверяйте переменные: env | sort в терминале vs. задача * * * * * env | sort > /tmp/cron_env.txt.

Перенаправление вывода и ротация логов

По умолчанию cron отправляет вывод (stdout, stderr) пользователю по email. Если MAILTO="", письма отключаются.

# отправить вывод в файл
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# добавить дату в лог (читаемость)
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# rotate старых логов
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1 && \
  find /var/log -name "backup.log*" -mtime +7 -delete

Или используйте logger для syslog:

0 3 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup

Часовые пояса и TZ

Cron берёт системный часовой пояс. Если нужен другой:

# вариант 1: переменная в crontab
TZ=Europe/Moscow
0 9 * * * /opt/scripts/report.sh

# вариант 2: обёртка
0 9 * * * TZ=Europe/Moscow /opt/scripts/report.sh
Предупреждение

TZ влияет только на расписание. Внутри скрипта используйте $TZ явно: date +%Z покажет системный пояс.

Проверить расписание по UTC:

crontab -l | while read line; do
  if [[ ! "$line" =~ ^# ]] && [[ ! -z "$line" ]]; then
    echo "$line" | awk '{print $1":"$2" UTC  |  "$5" "$6" "$7" "$8" "$9" "$10}'
  fi
done

Типовые подводные камни и проверка перед запуском

1. Символ % в команде

% в crontab — это перенос строки. Экранируйте:

# Неправильно:
0 3 * * * /opt/scripts/report.sh "Report for $(date +%Y-%m-%d)"

# Правильно:
0 3 * * * /opt/scripts/report.sh "Report for $(date +\%Y-\%m-\%d)"

2. Наложение задач

Если скрипт может выполняться дольше интервала, нужен lock:

# /opt/scripts/long-task.sh
LOCKFILE=/var/run/long-task.lock

if [ -f "$LOCKFILE" ]; then
  echo "Already running" >&2
  exit 1
fi

trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"

# основная логика
sleep 30

3. Проверка перед деплоем

# показать ближайший запуск каждой задачи
for f in /etc/cron.d/*; do
  if [ -f "$f" ]; then
    echo "=== $f ==="
    head -1 "$f"
    # ближайшее время запуска
    next=$(echo "0 3 * * *" | sed 's/\*/0/g' | xargs -I{} date -d "{}" '+%Y-%m-%d %H:%M')
    echo "Next: $next"
  fi
done

# тестовый запуск
cat /etc/cron.d/my-task | grep -v "^#" | grep -v "^$" | while read schedule cmd; do
  echo "Would run: $cmd"
done

4. Синтаксис и валидация

# проверить формат crontab
crontab -l | grep -v "^#" | grep -v "^$" | awk '{print $1" "$2" "$3" "$4" "$5}' | \
  while read min hour dom mon dow; do
    # базовая проверка
    echo "$min $hour $dom $mon $dow"
  done

Ansible и cron: idempotent установка задач

- name: Add backup cron job
  community.general.cron:
    name: "backup database"
    minute: "0"
    hour: "3"
    job: "/opt/scripts/backup.sh >> /var/log/backup.log 2>&1"
    user: "root"
    state: present
    cron_file: "backup"
# Удалить задачу
- name: Remove old cron job
  community.general.cron:
    name: "obsolete task"
    state: absent
    user: "root"
# Положить файл в /etc/cron.d/
- name: Deploy cron file
  ansible.builtin.copy:
    src: files/my-cron-job
    dest: /etc/cron.d/my-cron-job
    owner: root
    group: root
    mode: "0644"
  notify: restart cron

systemd timers как альтернатива cron

Timers точнее, умеют в зависимости от сервисов, поддерживают randomiseddelaysec и calendar specs.

# /etc/systemd/system/backup.service
[Unit]
Description=Backup database

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3am

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --all | grep backup

Преимущества timers:

  • зависимости (After=network.target)
  • логи через journal (journalctl -u backup.service)
  • randomiseddelaysec для рандомизации (избежать thundering herd)
  • one-shot и monotonic таймеры

Cron остаётся проще для базовых задач. systemd-таймеры — выбор для сервисов с зависимостями и мониторингом через journald.