Running a playbook manually is a proof of concept. Running it on a schedule with structured output, failure alerts, audit logs, and a compliance dashboard is operational patch management. The gap between the two is where most automation efforts stall—not because the tooling is missing, but because the reporting and scheduling layers never get built.
Scheduling Options
| Method | Best for |
|---|---|
| Cron | Simple, single-host control nodes; no UI required |
| AWX / AAP | Teams; role-based access; inventory management; job history |
| CI/CD (GitHub Actions / GitLab) | Infrastructure-as-code workflows; change-controlled patching |
Cron example (patch every Sunday at 02:00):
crontab -e
# Add:
0 2 * * 0 /usr/bin/ansible-playbook -i /opt/ansible/inventory/production/hosts.yml \
/opt/ansible/playbooks/patch-linux.yml \
--vault-password-file /opt/ansible/.vault_pass \
>> /var/log/ansible-patch.log 2>&1
AWX / Ansible Automation Platform — create a Job Template pointing at your playbook and inventory, then attach a Schedule with a cron expression through the UI or API. AWX stores every job run, its output, and its status, giving you an audit log without any extra tooling.
Structured Logging with Callbacks
Ansible’s callback plugins transform run output into structured formats for storage or downstream processing.
Enable JSON logging in ansible.cfg:
[defaults]
stdout_callback = json
callback_whitelist = json, timer, profile_tasks
Or write to a log file:
[defaults]
log_path = /var/log/ansible/patch.log
For per-run JSON reports, use the json callback and redirect output:
ANSIBLE_STDOUT_CALLBACK=json ansible-playbook patch-linux.yml \
-i inventory/production/hosts.yml \
> /var/log/ansible/patch-$(date +%Y%m%d-%H%M%S).json
Patch Compliance Reporting
Generate a per-host compliance summary by registering results and writing them to a central location.
---
- name: Collect patch compliance data
hosts: all
become: true
gather_facts: true
tasks:
- name: Get list of available security updates (Debian/Ubuntu)
ansible.builtin.shell: apt-get --just-print upgrade 2>/dev/null | grep ^Inst | wc -l
register: pending_updates
changed_when: false
when: ansible_os_family == "Debian"
- name: Get last apt history entry
ansible.builtin.shell: grep "Start-Date" /var/log/apt/history.log | tail -1
register: last_patch_date
changed_when: false
when: ansible_os_family == "Debian"
- name: Write compliance report entry
ansible.builtin.lineinfile:
path: /tmp/patch_compliance_.csv
line: ",,,,"
create: true
delegate_to: localhost
- name: Consolidate compliance report
hosts: localhost
gather_facts: false
tasks:
- name: Add CSV header
ansible.builtin.lineinfile:
path: /tmp/patch_compliance_.csv
insertbefore: BOF
line: "hostname,distro,version,pending_updates,last_patched"
SIEM Integration
Push patch results to a SIEM or log aggregator using the uri module or syslog.
POST to an HTTP endpoint (Splunk HEC, Elastic, etc.):
- name: Send patch result to SIEM
ansible.builtin.uri:
url: ""
method: POST
headers:
Authorization: "Splunk "
Content-Type: application/json
body_format: json
body:
event:
host: ""
source: ansible-patch
timestamp: ""
updates_installed: ""
reboot_required: ""
status: "success"
status_code: 200
delegate_to: localhost
Write to syslog on each host:
- name: Log patch result to syslog
ansible.builtin.command: >
logger -t ansible-patch -p local0.info
"host= status=patched updates="
changed_when: false
Slack and Email Notifications
Slack (using community.general):
- name: Notify Slack on patch completion
community.general.slack:
token: ""
channel: "#patch-ops"
msg: >
*Patch run completed* —
Hosts:
Failed:
delegate_to: localhost
run_once: true
Email (using community.general.mail):
- name: Send patch summary email
community.general.mail:
host: ""
port: 587
username: ""
password: ""
to: ops-team@example.com
subject: "Ansible Patch Report "
body: ""
delegate_to: localhost
run_once: true
Drift Detection and Remediation
Drift means a host is no longer in the expected state—a package was manually installed, a service was changed outside of Ansible, or an update was applied through a different channel.
Detect drift with --check --diff on a scheduled basis:
# Run daily in check mode — alert if any changes would be made (i.e., drift found)
ansible-playbook -i inventory/production/hosts.yml playbooks/baseline.yml \
--check --diff 2>&1 | tee /tmp/drift-$(date +%Y%m%d).log
# Alert if drift detected
if grep -q "changed=" /tmp/drift-$(date +%Y%m%d).log; then
echo "Drift detected — review /tmp/drift-$(date +%Y%m%d).log" | \
mail -s "Ansible drift alert" ops-team@example.com
fi
Automated remediation — run the playbook without --check on the same schedule to continuously enforce the desired state. This is appropriate for configuration items (file contents, service states) but requires careful thought before applying to patch state, where a forced upgrade could break compatibility.
Audit Trail Best Practices
- Version control all playbooks and inventories in Git; every change is attributed and timestamped.
- Use AWX or Automation Platform for a persistent job history accessible without SSH.
- Store per-run JSON logs in an append-only location (object storage or SIEM) to satisfy audit requirements.
- Log
becomeescalations: who ran which playbook, on which inventory, at what time, from which control node. - Rotate but retain logs for the period required by your compliance framework (commonly 12 months for PCI-DSS, 7 years for some financial regulations).
Previous: Windows Server Patching with Ansible
Next in the series: Production Checklist and Maturity Roadmap — pre/during/post patching checklists, common pitfalls, and the path from manual to fully automated compliance-driven operations.
