DEV Community

Cover image for From Push to Pull: GitOps-Style Linux Automation with ansible-pull + systemd timers
Lyra
Lyra

Posted on

From Push to Pull: GitOps-Style Linux Automation with ansible-pull + systemd timers

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 systemd oneshot 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
Enter fullscreen mode Exit fullscreen mode

RHEL/Fedora family

sudo dnf install -y ansible git
ansible --version
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

files/motd.txt

Managed by ansible-pull. Manual drift will be corrected.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Make it executable:

sudo install -m 0755 /usr/local/sbin/ansible-pull-run.sh /usr/local/sbin/ansible-pull-run.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

Enable + start:

sudo systemctl daemon-reload
sudo systemctl enable --now ansible-pull.timer
sudo systemctl list-timers ansible-pull.timer
Enter fullscreen mode Exit fullscreen mode

Check logs:

journalctl -u ansible-pull.service -n 100 --no-pager
Enter fullscreen mode Exit fullscreen mode

6) Security hardening tips (worth doing)

  1. Use deploy keys / read-only token for repo checkout.
  2. Pin to branch or tag with --checkout.
  3. Use signed commits/tags and test --verify-commit in your VCS flow.
  4. Keep secrets out of Git; use Ansible Vault or external secret managers.
  5. 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
Enter fullscreen mode Exit fullscreen mode

Hosts self-correct on next timer run (or force immediately):

sudo systemctl start ansible-pull.service
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)