Ansible ansible-pull — Local Playbook Execution from Git

Introduction

ansible-pull inverts the default push model. Instead of a central controller pushing configs to nodes, each node pulls its playbook from a Git repository and runs it locally. This is ideal for workstation setup, auto-scaling instances, edge devices, and any scenario where a central controller is impractical.

Basic Usage

# Pull and run a playbook from Git
ansible-pull -U https://github.com/org/ansible-config.git playbook.yml

# With SSH key authentication
ansible-pull -U git@github.com:org/ansible-config.git playbook.yml

# Specific branch
ansible-pull -U https://github.com/org/ansible-config.git -C production playbook.yml

# Run only if repo has changed
ansible-pull -U https://github.com/org/ansible-config.git --only-if-changed playbook.yml

How It Works

1. ansible-pull runs on the TARGET machine (not controller)
2. Clones/updates the Git repository locally
3. Runs the specified playbook against localhost
4. Optionally schedules itself via cron

┌─────────────────┐     git pull     ┌──────────────┐
│  Target Machine  │ ◄────────────── │  Git Repo    │
│  (runs locally)  │                 │  (playbooks) │
└─────────────────┘                  └──────────────┘

Repository Structure

ansible-config/
├── local.yml              # Default playbook (auto-detected)
├── hosts                  # Inventory (optional, usually localhost)
├── requirements.yml       # Galaxy roles/collections
├── group_vars/
│   └── all.yml
├── host_vars/
│   └── workstation.yml
├── roles/
│   ├── common/
│   ├── dev-tools/
│   └── security/
└── files/
    └── bashrc

Workstation Setup Example

# local.yml — automatically detected by ansible-pull
---
- name: Configure workstation
  hosts: localhost
  connection: local
  become: true

  vars:
    username: "{{ lookup('env', 'USER') }}"
    dev_packages:
      - git
      - curl
      - vim
      - tmux
      - htop
      - jq
      - ripgrep
      - fd-find

  tasks:
    - name: Update package cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

    - name: Install development packages
      ansible.builtin.package:
        name: "{{ dev_packages }}"
        state: present

    - name: Install Docker
      ansible.builtin.include_role:
        name: docker
      when: "'docker' not in ansible_facts.packages"

    - name: Configure Git
      ansible.builtin.template:
        src: files/gitconfig.j2
        dest: "/home/{{ username }}/.gitconfig"
        owner: "{{ username }}"
        mode: '0644'

    - name: Configure SSH
      ansible.builtin.file:
        path: "/home/{{ username }}/.ssh"
        state: directory
        owner: "{{ username }}"
        mode: '0700'

    - name: Set timezone
      community.general.timezone:
        name: Europe/London

    - name: Enable firewall
      community.general.ufw:
        state: enabled
        default: deny
        rule: allow
        port: "22"

Cron-Based Pull

# Set up cron to run ansible-pull every 30 minutes
ansible-pull -U https://github.com/org/ansible-config.git \
  -C main \
  --only-if-changed \
  -i localhost, \
  -d /opt/ansible-config \
  --sleep 60 \
  local.yml

# Add to crontab
crontab -e
# */30 * * * * /usr/bin/ansible-pull -U https://github.com/org/ansible-config.git --only-if-changed -d /opt/ansible-config local.yml >> /var/log/ansible-pull.log 2>&1

Self-Installing Cron

# The playbook installs its own cron job
- name: Set up ansible-pull cron
  ansible.builtin.cron:
    name: "ansible-pull"
    minute: "*/30"
    job: >
      /usr/bin/ansible-pull
      -U {{ repo_url }}
      -C {{ branch | default('main') }}
      --only-if-changed
      -d /opt/ansible-config
      local.yml
      >> /var/log/ansible-pull.log 2>&1
    user: root

Cloud-Init Bootstrap

# cloud-init user-data — bootstrap ansible-pull on first boot
#cloud-config
packages:
  - ansible
  - git

runcmd:
  - ansible-pull -U https://github.com/org/ansible-config.git -C main local.yml

Key Options

OptionDescription
-U <url>Git repository URL (required)
-C <branch>Checkout specific branch/tag
-d <dir>Local directory to clone into
--only-if-changedOnly run playbook if repo changed
--sleep <sec>Random sleep before run (prevents thundering herd)
-i localhost,Use localhost inventory
--accept-host-keyAccept SSH host key on first connect
-e key=valExtra variables
--vault-password-filePath to vault password file
--purgeDelete local repo after run

Hostname-Based Configuration

# Apply different configs based on hostname
---
- name: Base configuration
  hosts: localhost
  connection: local
  become: true
  roles:
    - common
    - security

- name: Web server configuration
  hosts: localhost
  connection: local
  become: true
  roles:
    - nginx
  when: "'web' in ansible_hostname"

- name: Database configuration
  hosts: localhost
  connection: local
  become: true
  roles:
    - postgresql
  when: "'db' in ansible_hostname"

Logging

# Redirect output to syslog
ansible-pull -U ... local.yml 2>&1 | logger -t ansible-pull

# Or to a dedicated log file
ansible-pull -U ... local.yml >> /var/log/ansible-pull.log 2>&1

# Log rotation
cat > /etc/logrotate.d/ansible-pull << EOF
/var/log/ansible-pull.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
}
EOF

Troubleshooting

IssueSolution
Git clone failsCheck SSH keys or use HTTPS with token
"No hosts matched"Use -i localhost, (note trailing comma)
Permission deniedRun with sudo or become: true
Cron not runningCheck cron.log, verify ansible-pull path
Vault password neededUse --vault-password-file /root/.vault_pass
Too many simultaneous pullsUse --sleep 300 for random delay

Pull vs Push: When to Use Each

CriteriaPush (ansible-playbook)Pull (ansible-pull)
ControlCentral controllerEach node self-manages
ScaleHundreds of hostsThousands (no controller bottleneck)
NetworkController → targetsTargets → Git repo
Use caseServer fleet managementWorkstations, auto-scaling, edge
FeedbackImmediate outputCheck logs on each node

Best Practices

  1. Use --only-if-changed — skip playbook run if Git repo hasn't changed
  2. Add --sleep — random delay prevents all nodes pulling at once
  3. Use local.yml — auto-detected filename, no need to specify
  4. Log everything — redirect to file + logrotate
  5. Bootstrap via cloud-init — first-boot installs Ansible and runs pull
  6. Tag the repo — use -C v1.2.3 for pinned versions in production

Conclusion

ansible-pull is perfect when a central controller doesn't make sense — workstation setup, auto-scaling groups, edge devices, and developer machines. Combined with cron and --only-if-changed, nodes self-configure continuously from a Git repository with zero manual intervention.