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

Индекс LLMS: [llms.txt](/llms.txt)

---

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        # загрузить задачи из файла
```

> [!NOTE]
> По умолчанию редактор — `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
```

> [!WARNING]
> Не добавляйте задачи напрямую в `/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
```

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

```bash
#!/bin/bash
# /opt/scripts/run_backup.sh
source /etc/profile
cd /opt/project || exit 1
./backup.sh
```

> [!TIP]
> Всегда проверяйте переменные: `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
```

> [!WARNING]
> 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:

```bash
# /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. Проверка перед деплоем**

```bash
# показать ближайший запуск каждой задачи
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. Синтаксис и валидация**

```bash
# проверить формат 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 установка задач

```yaml
- 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"
```

```yaml
# Удалить задачу
- name: Remove old cron job
  community.general.cron:
    name: "obsolete task"
    state: absent
    user: "root"
```

```yaml
# Положить файл в /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.

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

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

[Install]
WantedBy=multi-user.target
```

```ini
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3am

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

[Install]
WantedBy=timers.target
```

```bash
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.
