Configure centralized cron management with Ansible automation and systemd timers

Intermediate 45 min May 13, 2026 47 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated cron job deployment and monitoring across multiple servers using Ansible playbooks with systemd timers for reliable task scheduling and centralized logging.

Prerequisites

  • Root access to control and target servers
  • Basic understanding of SSH and Linux system administration
  • Multiple servers for centralized management

What this solves

Managing cron jobs across multiple servers becomes complex when you need consistent scheduling, monitoring, and logging. This tutorial shows you how to centralize cron management using Ansible to deploy systemd timers automatically, providing better logging, dependency handling, and failure detection than traditional cron.

Step-by-step configuration

Install Ansible on the control node

Start by installing Ansible on your management server where you'll run the playbooks from.

sudo apt update
sudo apt install -y ansible python3-pip
pip3 install ansible-core
sudo dnf install -y epel-release
sudo dnf install -y ansible python3-pip
pip3 install ansible-core

Create Ansible inventory file

Define your target servers in an inventory file for Ansible to manage.

[cron_servers]
web1.example.com ansible_host=203.0.113.10
web2.example.com ansible_host=203.0.113.11
db1.example.com ansible_host=203.0.113.12

[all:vars]
ansible_user=ansible
ansible_ssh_private_key_file=~/.ssh/ansible_key

Configure SSH key authentication

Set up passwordless SSH access from your Ansible control node to all managed servers.

ssh-keygen -t ed25519 -f ~/.ssh/ansible_key -N ""
ssh-copy-id -i ~/.ssh/ansible_key.pub ansible@203.0.113.10
ssh-copy-id -i ~/.ssh/ansible_key.pub ansible@203.0.113.11
ssh-copy-id -i ~/.ssh/ansible_key.pub ansible@203.0.113.12

Create the cron management playbook directory

Organize your Ansible playbooks with a proper directory structure for templates and tasks.

mkdir -p ~/ansible-cron/{playbooks,templates,group_vars}
cd ~/ansible-cron

Create systemd timer template

This template generates systemd timer files for scheduled tasks with better logging than cron.

[Unit]
Description={{ item.description | default('Scheduled task') }}
Requires={{ item.name }}.service

[Timer]
OnCalendar={{ item.schedule }}
Persistent=true
RandomizedDelaySec={{ item.randomized_delay | default('0') }}

[Install]
WantedBy=timers.target

Create systemd service template

This template defines the actual commands that run when the timer triggers.

[Unit]
Description={{ item.description | default('Scheduled task service') }}
After=network.target

[Service]
Type=oneshot
User={{ item.user | default('root') }}
Group={{ item.group | default('root') }}
WorkingDirectory={{ item.working_directory | default('/tmp') }}
ExecStart={{ item.command }}
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ item.name }}
{% if item.environment is defined %}
{% for env_var in item.environment %}
Environment={{ env_var }}
{% endfor %}
{% endif %}

[Install]
WantedBy=multi-user.target

Define cron jobs configuration

Create a YAML file to define all your scheduled tasks with their parameters.

---
cron_jobs:
  - name: backup-database
    description: "Daily database backup"
    command: "/usr/local/bin/backup-db.sh"
    schedule: "daily"
    user: "backup"
    group: "backup"
    working_directory: "/var/backups"
    environment:
      - "BACKUP_RETENTION=7"
      - "BACKUP_COMPRESSION=gzip"
    
  - name: log-cleanup
    description: "Weekly log file cleanup"
    command: "/usr/local/bin/cleanup-logs.sh"
    schedule: "weekly"
    user: "root"
    randomized_delay: "1h"
    
  - name: system-update-check
    description: "Check for system updates every 4 hours"
    command: "/usr/local/bin/check-updates.sh"
    schedule: "--* 00,04,08,12,16,20:00:00"
    user: "root"
    
  - name: web-health-check
    description: "Monitor web application health every 5 minutes"
    command: "/usr/local/bin/health-check.sh"
    schedule: "*:0/5"
    user: "monitor"
    group: "monitor"
    working_directory: "/opt/monitoring"

Create the main playbook

This playbook deploys systemd timers and services to all target servers based on your configuration.

---
  • name: Deploy centralized cron management with systemd timers
hosts: cron_servers become: yes tasks: - name: Create systemd service files template: src: systemd-service.j2 dest: "/etc/systemd/system/{{ item.name }}.service" mode: '644' owner: root group: root loop: "{{ cron_jobs }}" notify: - Reload systemd - Restart services - name: Create systemd timer files template: src: systemd-timer.j2 dest: "/etc/systemd/system/{{ item.name }}.timer" mode: '644' owner: root group: root loop: "{{ cron_jobs }}" notify: - Reload systemd - Restart timers - name: Create log directory for scheduled tasks file: path: /var/log/scheduled-tasks state: directory mode: '755' owner: root group: root - name: Configure rsyslog for scheduled task logging copy: content: | # Log scheduled tasks to separate file if $programname startswith 'backup-database' then /var/log/scheduled-tasks/backup.log if $programname startswith 'log-cleanup' then /var/log/scheduled-tasks/cleanup.log if $programname startswith 'system-update-check' then /var/log/scheduled-tasks/updates.log if $programname startswith 'web-health-check' then /var/log/scheduled-tasks/health.log & stop dest: /etc/rsyslog.d/50-scheduled-tasks.conf mode: '644' notify: - Restart rsyslog - name: Create users for scheduled tasks user: name: "{{ item.user }}" system: yes shell: /bin/false home: /var/lib/{{ item.user }} create_home: yes loop: "{{ cron_jobs }}" when: item.user != 'root' handlers: - name: Reload systemd systemd: daemon_reload: yes - name: Restart services systemd: name: "{{ item.name }}.service" state: restarted enabled: yes loop: "{{ cron_jobs }}" - name: Restart timers systemd: name: "{{ item.name }}.timer" state: restarted enabled: yes loop: "{{ cron_jobs }}" - name: Restart rsyslog service: name: rsyslog state: restarted

Create monitoring playbook

This playbook helps you monitor the status of all scheduled tasks across your infrastructure.

---
  • name: Monitor systemd timers and services
hosts: cron_servers become: yes tasks: - name: Check timer status command: systemctl is-active {{ item.name }}.timer register: timer_status loop: "{{ cron_jobs }}" failed_when: false changed_when: false - name: Check service status command: systemctl is-enabled {{ item.name }}.timer register: service_enabled loop: "{{ cron_jobs }}" failed_when: false changed_when: false - name: Get last run information command: systemctl show {{ item.name }}.timer --property=LastTriggerUSec register: last_run loop: "{{ cron_jobs }}" changed_when: false - name: Display timer status debug: msg: | Timer: {{ item.item.name }} Status: {{ item.stdout }} Last run: {{ last_run.results[ansible_loop.index0].stdout.split('=')[1] }} loop: "{{ timer_status.results }}" loop_control: extended: yes

Deploy the configuration

Run the Ansible playbook to deploy systemd timers to all your servers.

cd ~/ansible-cron
ansible-playbook -i /etc/ansible/hosts playbooks/deploy-cron.yml
ansible-playbook -i /etc/ansible/hosts playbooks/monitor-cron.yml

Set up log rotation for scheduled tasks

Configure logrotate to manage log files from your scheduled tasks.

/var/log/scheduled-tasks/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    copytruncate
    notifempty
    sharedscripts
    postrotate
        /bin/kill -HUP cat /var/run/rsyslogd.pid 2>/dev/null 2>/dev/null || true
    endscript
}

Add logrotate configuration to playbook

Update your main playbook to include log rotation configuration.

cat >> ~/ansible-cron/playbooks/deploy-cron.yml << 'EOF'
    - name: Configure log rotation for scheduled tasks
      template:
        src: logrotate-scheduled.j2
        dest: /etc/logrotate.d/scheduled-tasks
        mode: '644'
        owner: root
        group: root
EOF

Configure centralized monitoring

Create status reporting playbook

Generate consolidated reports on all scheduled task execution across your infrastructure.

---
  • name: Generate cron status report
hosts: cron_servers become: yes gather_facts: yes tasks: - name: Get systemd timer list shell: systemctl list-timers --all --no-pager --plain | grep -E '(backup-database|log-cleanup|system-update-check|web-health-check)' register: timer_list failed_when: false changed_when: false - name: Check for failed services in last 24 hours shell: journalctl --since "24 hours ago" --grep "Failed to start" | grep -E '(backup-database|log-cleanup|system-update-check|web-health-check)' register: failed_services failed_when: false changed_when: false - name: Generate status report copy: content: | Server: {{ inventory_hostname }} Date: {{ ansible_date_time.iso8601 }} Active Timers: {{ timer_list.stdout }} Failed Services (24h): {{ failed_services.stdout | default('No failures') }} System Load: {{ ansible_loadavg }} Memory Usage: {{ ansible_memory_mb.real.used }} MB / {{ ansible_memory_mb.real.total }} MB dest: "/tmp/cron-status-{{ inventory_hostname }}.txt" - name: Fetch status reports fetch: src: "/tmp/cron-status-{{ inventory_hostname }}.txt" dest: "./reports/" flat: yes

Create alert configuration for failed tasks

Set up email alerts when scheduled tasks fail using systemd's OnFailure directive.

[Unit]
Description=Send alert for failed {{ item.name }}

[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-failure-alert.sh "{{ item.name }}" "{{ item.description }}"
User=root

Verify your setup

# Check Ansible can reach all hosts
ansible all -i /etc/ansible/hosts -m ping

View active timers on all servers

ansible cron_servers -i /etc/ansible/hosts -b -m shell -a "systemctl list-timers --active"

Check logs for specific service

ansible web1.example.com -i /etc/ansible/hosts -b -m shell -a "journalctl -u backup-database.service -n 10"

Generate comprehensive status report

ansible-playbook -i /etc/ansible/hosts playbooks/status-report.yml ls -la reports/

Advanced configuration options

Note: For comprehensive monitoring of your cron jobs and systemd timers, consider implementing Prometheus monitoring with Grafana alerting for real-time visibility.
ConfigurationPurposeExample
RandomizedDelaySecPrevent all servers from running tasks simultaneouslyRandomizedDelaySec=300
OnFailureDefine actions when service failsOnFailure=failure-alert@%i.service
PersistentRun missed tasks after system bootPersistent=true
AccuracySecTimer accuracy for resource optimizationAccuracySec=5min

Common issues

SymptomCauseFix
Timer shows inactiveTimer not enabledsystemctl enable --now timer-name.timer
Service fails immediatelyCommand path not foundUse absolute paths in ExecStart
Ansible playbook failsSSH key authentication issuesCheck ssh -i ~/.ssh/ansible_key ansible@host
Logs not appearingRsyslog not restartedsystemctl restart rsyslog
Permission denied errorsWrong user in service fileSet correct User= and Group= in service template
Timer runs too frequentlyInvalid OnCalendar formatTest with systemd-analyze calendar "schedule"

Next steps

Running this in production?

Want this handled for you? Setting up centralized cron management once is straightforward. Keeping it monitored, backed up, and scaling across environments as your infrastructure grows is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.