Как работает ansible cron и почему имя - это всё
Сначала вспомним механику самого cron. Демон crond читает crontab-файлы и раз в минуту проверяет, не пора ли что-то запустить. Строка расписания - это пять полей: минута, час, день месяца, месяц, день недели, потом сама команда.
Код: Выделить всё
# m h dom mon dow command
30 3 * * * /usr/local/bin/backup.sh
Теперь главное про планировщик ansible. Модуль ansible.builtin.cron не просто дописывает строку в crontab - он управляет ею по идентификатору. И этот идентификатор - параметр name. Ansible вставляет над задачей служебный комментарий вида "#Ansible: backup nightly", и при следующих запусках ищет именно его. Совпало имя - задача уже есть, ничего не делает (ok). Поменялось расписание - перепишет строку (changed). Поставил state: absent - удалит. Вот как выглядит правильная ansible cron job:
Код: Выделить всё
- name: Ночной бэкап в кроне пользователя root
ansible.builtin.cron:
name: "nightly backup"
minute: "30"
hour: "3"
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
user: root
state: present
Поля minute/hour/day/month/weekday по умолчанию равны "*". Удобно: если хочешь "каждый час в 0 минут", задаёшь только minute: "0", остальное само станет звёздочкой. Для спецвремён есть отдельный параметр special_time (значения reboot, hourly, daily, weekly, monthly, annually/yearly):
Код: Выделить всё
- name: Чистка временных файлов раз в сутки
ansible.builtin.cron:
name: "cleanup tmp"
special_time: daily
job: "find /var/tmp -type f -mtime +7 -delete"
user: root

Файл в /etc/cron.d и переменные окружения: ansible crontab по-взрослому
Класть всё в личный crontab root - не всегда удобно. На проде чище держать задачу отдельным файлом в /etc/cron.d/ - его видно, легко выгрести в системе контроля версий, легко удалить целиком. Для этого у модуля есть параметр cron_file. Важный нюанс, на котором многие спотыкаются: путь указывается относительно /etc/cron.d, без слеша в начале. Напишешь абсолютный путь - получишь не то. И второе - в формате /etc/cron.d строка обязана содержать пользователя, поэтому параметр user тут не опционален, его требует сам модуль.
Код: Выделить всё
- name: Бэкап как файл в /etc/cron.d
ansible.builtin.cron:
name: "nightly backup"
cron_file: backup # это /etc/cron.d/backup
user: root
minute: "30"
hour: "3"
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
state: present
Код: Выделить всё
#Ansible: nightly backup
30 3 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Часто скрипту нужно окружение: правильный PATH, MAILTO для отчётов об ошибках, какая-нибудь переменная для бэкап-утилиты. Тот же модуль умеет управлять и переменными - через env: true. Тогда name трактуется как имя переменной, а job - как её значение:
Код: Выделить всё
- name: PATH для задач в cron.d
ansible.builtin.cron:
cron_file: backup
user: root
name: PATH
env: true
job: "/usr/local/bin:/usr/bin:/bin"
- name: Слать вывод на почту админа
ansible.builtin.cron:
cron_file: backup
user: root
name: MAILTO
env: true
job: "ops@example.org"
Маленькая заметка про дистрибутивы. Всё вышеописанное одинаково работает на RHEL, Fedora, Rocky/Alma, а также на RED OS и Astra Linux - формат crontab стандартный. На Debian/Ubuntu тоже всё то же, разница лишь в том, что пакет называется cron, а в RHEL-семействе - cronie (ставится через dnf install cronie, сервис crond). Сам модуль про это не думает, он работает с файлами.
systemd timers через Ansible и at для разовых задач
Cron - проверенная классика, но у systemd есть свой планировщик - таймеры. Чем ansible systemd timer лучше cron в ряде случаев: подробное логирование через journalctl, зависимости между юнитами, опция Persistent=true (пропущенный из-за выключенного сервера запуск выполнится при старте), точность и случайные сдвиги (RandomizedDelaySec), чтобы тридцать серверов не ломанулись делать бэкап в одну секунду.
Готового модуля "сделай мне таймер" нет - и не надо. Таймер в systemd это пара файлов: юнит .service (что запускать) и юнит .timer (когда). Раскладываем их шаблоном ansible.builtin.template (или copy), затем включаем через ansible.builtin.systemd_service. Вот связка целиком:
Код: Выделить всё
- name: Service-юнит бэкапа
ansible.builtin.copy:
dest: /etc/systemd/system/backup.service
content: |
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
owner: root
group: root
mode: "0644"
- name: Timer-юнит бэкапа
ansible.builtin.copy:
dest: /etc/systemd/system/backup.timer
content: |
[Unit]
Description=Run backup daily at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
owner: root
group: root
mode: "0644"
- name: Включить и запустить таймер
ansible.builtin.systemd_service:
name: backup.timer
enabled: true
state: started
daemon_reload: true
И последнее - разовые задачи. Если надо запустить что-то один раз через два часа, cron избыточен, для этого есть at. Готового модуля тоже нет, гоним через ansible.builtin.command (не забыв про идемпотентность - command всегда changed, поэтому ставим защиту):
Код: Выделить всё
- name: Разовый прогрев кеша через at
ansible.builtin.command:
cmd: "at now + 2 hours -f /usr/local/bin/warmup.sh"
register: at_res
changed_when: at_res.rc == 0
- Нет параметра name - дубли в crontab при каждом изменении job. Лечится только именем.
- Абсолютный путь в cron_file (например /etc/cron.d/backup). Нужно относительное: cron_file: backup.
- Забыл user при cron_file - модуль выдаст ошибку, в формате cron.d пользователь обязателен.
- В скрипте бэкапа сломанный или пустой PATH. Cron запускает с очень скудным окружением, поэтому в самом скрипте пиши абсолютные пути к бинарникам или задавай PATH через env-переменную.
- Создал systemd timer, но забыл daemon_reload и enabled - юнит лежит, но не тикает.
- SELinux: если бэкап пишет в нестандартный каталог, проверь контекст (ls -Z, при необходимости restorecon или semanage fcontext), иначе скрипт молча упрётся в Permission denied при включённом enforcing.
Возьми пару тестовых хостов (или контейнеров) и руками повтори:
- Напиши плейбук, который кладёт /usr/local/bin/backup.sh (через copy) и заводит ansible cron job на 03:30 с name. Прогони дважды, проверь crontab -l - задача одна.
- Перенеси ту же задачу в /etc/cron.d через cron_file, добавь env-переменные PATH и MAILTO. Глянь содержимое /etc/cron.d/backup.
- Поставь disabled: true и убедись, что строка закомментировалась, потом верни обратно.
- Сделай тот же бэкап через systemd timer: два юнита + systemd_service. Проверь systemctl list-timers и journalctl -u backup.service.
- Почему параметр name критичен для идемпотентности модуля cron и что произойдёт без него?
- Чем путь в cron_file отличается от обычного и какой ещё параметр становится обязательным при работе с /etc/cron.d?
- Какие два юнита нужны для systemd timer и почему мы запускаем именно .timer, а не .service?
- Что даёт Persistent=true в таймере и какого аналога нет у классического cron?
Планировщик через Ansible - это про предсказуемость. Модуль ansible.builtin.cron с обязательным name даёт идемпотентные задачи, cron_file выносит их в аккуратный файл /etc/cron.d, env-переменные чинят окружение. Где нужны логи, зависимости и устойчивость к простоям - бери systemd timers через template + systemd_service, для разовых дел - at. На RHCE спросят именно cron-модуль, так что доведи работу с name и расписанием до автоматизма.