If you manage one Linux server, traditional “push Ansible from your laptop” works fine.
If you manage many intermittently online systems (laptops, edge nodes, remote boxes), push starts to feel brittle.
A cleaner pattern is pull-based automation:
- each host periodically pulls your Git repo,
- applies a local playbook,
- and self-heals drift on schedule.
This post shows a practical setup with:
ansible-pull- a minimal playbook
- a
systemdoneshot service + timer - optional commit-signature verification
Why ansible-pull?
ansible-pull inverts Ansible’s default push model: each node checks out a repo and runs locally.
This is useful when:
- nodes are not always reachable inbound,
- you want Git as the source of truth,
- you want periodic remediation without a central control node always online.
1) Install Ansible on the target host
Debian/Ubuntu
sudo apt update
sudo apt install -y ansible git
ansible --version
RHEL/Fedora family
sudo dnf install -y ansible git
ansible --version
2) Create a small automation repo
Create a repo (GitHub/GitLab/self-hosted Gitea all fine) with this structure:
infra-pull/
├── local.yml
└── files/
└── motd.txt
local.yml
---
- name: Local baseline
hosts: localhost
connection: local
become: true
tasks:
- name: Ensure baseline packages are present (Debian example)
ansible.builtin.apt:
name:
- curl
- htop
- unattended-upgrades
state: present
update_cache: true
when: ansible_facts.os_family == "Debian"
- name: Ensure baseline packages are present (RedHat example)
ansible.builtin.dnf:
name:
- curl
- htop
state: present
when: ansible_facts.os_family == "RedHat"
- name: Deploy MOTD banner
ansible.builtin.copy:
src: files/motd.txt
dest: /etc/motd
owner: root
group: root
mode: "0644"
files/motd.txt
Managed by ansible-pull. Manual drift will be corrected.
Commit and push this repo.
3) Test ansible-pull manually first
Replace REPO_URL with your Git URL.
sudo ansible-pull \
--url "REPO_URL" \
--directory /var/lib/ansible-pull \
--checkout main \
--accept-host-key \
local.yml
Useful options you can add:
# Run playbook only if repository changed
--only-if-changed
# Discard local modifications in checkout
--clean
# Verify GPG signature of checked out commit (when supported)
--verify-commit
4) Add a dedicated wrapper script
Create /usr/local/sbin/ansible-pull-run.sh:
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="REPO_URL"
BRANCH="main"
WORKDIR="/var/lib/ansible-pull"
PLAYBOOK="local.yml"
/usr/bin/flock -n /run/ansible-pull.lock \
/usr/bin/ansible-pull \
--url "$REPO_URL" \
--directory "$WORKDIR" \
--checkout "$BRANCH" \
--accept-host-key \
--only-if-changed \
--clean \
"$PLAYBOOK"
Make it executable:
sudo install -m 0755 /usr/local/sbin/ansible-pull-run.sh /usr/local/sbin/ansible-pull-run.sh
Why flock? Official docs note Ansible CLI tools are not designed to run concurrently with themselves. This prevents overlap if one run is still active when the next starts.
5) Run it via systemd timer (not cron)
/etc/systemd/system/ansible-pull.service
[Unit]
Description=Apply local config with ansible-pull
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ansible-pull-run.sh
User=root
Group=root
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
/etc/systemd/system/ansible-pull.timer
[Unit]
Description=Run ansible-pull every 30 minutes
[Timer]
OnCalendar=*:0/30
Persistent=true
RandomizedDelaySec=3m
Unit=ansible-pull.service
[Install]
WantedBy=timers.target
Enable + start:
sudo systemctl daemon-reload
sudo systemctl enable --now ansible-pull.timer
sudo systemctl list-timers ansible-pull.timer
Check logs:
journalctl -u ansible-pull.service -n 100 --no-pager
6) Security hardening tips (worth doing)
- Use deploy keys / read-only token for repo checkout.
-
Pin to branch or tag with
--checkout. -
Use signed commits/tags and test
--verify-commitin your VCS flow. - Keep secrets out of Git; use Ansible Vault or external secret managers.
- Keep timer jitter (
RandomizedDelaySec) to avoid synchronized pull spikes.
7) Fast rollback pattern
Because config is Git-driven, rollback is straightforward:
git revert <bad_commit>
git push
Hosts self-correct on next timer run (or force immediately):
sudo systemctl start ansible-pull.service
Final thoughts
For small teams and solo operators, ansible-pull hits a sweet spot:
- Git-native,
- auditable,
- resilient to intermittent connectivity,
- and easy to scale from 1 node to many.
Push Ansible is still great. But for “systems that phone home,” pull is often the simpler mental model.
References
- Ansible docs:
ansible-pullCLI (options, behavior, concurrency note) https://docs.ansible.com/ansible/latest/cli/ansible-pull.html - Ansible docs: privilege escalation (
become) https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_privilege_escalation.html - Debian manpage:
systemd.timer(5)(OnCalendar,Persistent, timer behavior) https://manpages.debian.org/testing/systemd/systemd.timer.5.en.html -
flock(1)man page (advisory locking for non-overlapping runs) https://man7.org/linux/man-pages/man1/flock.1.html
Top comments (0)