AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible localhost & delegate_to: Run Tasks on Control Node Guide

By Luca Berton · Published 2024-01-01 · Category: installation

How to run Ansible tasks on localhost. Use connection local, delegate_to localhost, and local_action to execute on the Ansible control node.

Ansible localhost & delegate_to: Run Tasks on Control Node Guide

How to Execute command on the Ansible host?

When Ansible becomes part of your daily workflow it is natural you would like to automate also task in your local machine. I'm going to show you a live Playbook with some simple Ansible code. I'm Luca Berton and welcome to today's episode of Ansible Pilot.

See also: Ansible 'Failed to Connect via SSH localhost:22': Fix Guide

Execute command on the Ansible host options

connection plugindelegate_to: localhostlocal_action

There are three ways to execute modules and commands on the Ansible Controller host. The first and my favorite is using the connection plugin local and applying it to the Ansible Play level of your Playbook. The tricky was is to adjust some ansible variables about the python interpreter. I consider it the best way nowadays. The second way is using the delegate_to at the Task level. This has the advantage to delegate only one task to localhost but still needs only the implicit localhost scheme. The third way is using the local_action statement. I personally don't like it but it's one alternative as well at Task level, so same as the previous.

Links

Controlling where tasks run: delegation and local actionsImplicit ‘localhost’

## Playbook

How to Execute command on the Ansible host using connection: local method.

code

---
- name: localhost Playbook
  hosts: localhost
  vars:
    ansible_connection: local
    ansible_python_interpreter: "{{ ansible_playbook_python }}"
  tasks:
    - name: print hostname
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}"

execution

ansible-pilot $ ansible-playbook ansible\ statements/localhost.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'
PLAY [localhost Playbook] *****************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [localhost]
TASK [print hostname] *****************************************************************************
ok: [localhost] => {
    "msg": "localhost"
}
PLAY RECAP ****************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-pilot $

idempotency

ansible-pilot $ ansible-playbook ansible\ statements/localhost.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'
PLAY [localhost Playbook] *****************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [localhost]
TASK [print hostname] *****************************************************************************
ok: [localhost] => {
    "msg": "localhost"
}
PLAY RECAP ****************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-pilot $

See also: Ansible Cron Module: Schedule Jobs & Tasks on Linux (Complete Guide)

Conclusion

Now you know how to Execute commands and tasks on the Ansible localhost. You know how to use it based on your use case.

Methods to Run on Localhost

Method 1: hosts: localhost

---
- name: Run on controller
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Run local script
      ansible.builtin.command: ./scripts/setup.sh

Method 2: delegate_to localhost

- name: Configure remote servers
  hosts: webservers
  tasks:
    - name: Call API from controller
      ansible.builtin.uri:
        url: "http://api.example.com/servers/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost

- name: Install on remote (normal) ansible.builtin.package: name: nginx become: true

Method 3: local_action

- name: Shorter syntax
  hosts: webservers
  tasks:
    - name: Notify deployment
      local_action:
        module: ansible.builtin.uri
        url: https://hooks.slack.com/xxx
        method: POST
        body: '{"text": "Deploying to {{ inventory_hostname }}"}'
        body_format: json

See also: Ansible Read Files: lookup, slurp & fetch Module Guide

Common Use Cases

Health check before deployment

- name: Pre-deploy health check
  ansible.builtin.uri:
    url: "http://{{ inventory_hostname }}:8080/health"
  delegate_to: localhost
  register: health

- name: Deploy only if healthy ansible.builtin.git: repo: https://github.com/myorg/app.git dest: /opt/app when: health.status == 200

Generate files locally, deploy remotely

- name: Generate config on controller
  ansible.builtin.template:
    src: config.j2
    dest: "/tmp/{{ inventory_hostname }}-config.yml"
  delegate_to: localhost

- name: Copy to remote ansible.builtin.copy: src: "/tmp/{{ inventory_hostname }}-config.yml" dest: /etc/myapp/config.yml become: true

Wait for service after deploy

- name: Deploy application
  ansible.builtin.git:
    repo: https://github.com/myorg/app.git
    dest: /opt/app
  notify: restart app

- name: Flush handlers meta: flush_handlers

- name: Wait for app to be ready ansible.builtin.uri: url: "http://{{ inventory_hostname }}:8080/health" delegate_to: localhost register: result retries: 10 delay: 5 until: result.status == 200

localhost in Inventory

# Explicit localhost entry
all:
  hosts:
    localhost:
      ansible_connection: local
      ansible_python_interpreter: "{{ ansible_playbook_python }}"

delegate_to vs connection: local

| Approach | When to Use | |----------|-------------| | hosts: localhost | Entire play runs locally | | delegate_to: localhost | Single task runs locally | | connection: local | Set on play or in inventory | | local_action: | Shorthand for delegate_to localhost |

FAQ

Why does localhost need connection: local?

Without it, Ansible tries to SSH to localhost, which may fail or be slow. connection: local runs directly without SSH.

How do I run on localhost AND remote in the same play?

Use delegate_to for specific tasks:

- hosts: webservers
  tasks:
    - name: Local task
      command: echo "running locally"
      delegate_to: localhost
    - name: Remote task
      command: echo "running on {{ inventory_hostname }}"

Does delegate_to pass remote host variables?

Yes - the task has access to the remote host's variables even though it runs locally. That's why you can use {{ inventory_hostname }} in delegated tasks.

Run Entire Play on Localhost

- hosts: localhost
  connection: local
  gather_facts: true
  tasks:
    - debug: msg="Running on {{ inventory_hostname }}"
    - command: whoami
      register: who
    - debug: msg="{{ who.stdout }}"

delegate_to localhost

- hosts: webservers
  tasks:
    # Runs on EACH remote host
    - command: hostname

# Runs on controller for EACH host - uri: url: "http://lb.internal/api/register/{{ inventory_hostname }}" method: POST delegate_to: localhost

# Runs ONCE on controller - debug: msg="All hosts processed" run_once: true delegate_to: localhost

Local Actions

# Shorthand for delegate_to: localhost
- local_action:
    module: uri
    url: https://api.example.com/deploy
    method: POST
    body_format: json
    body:
      version: "{{ app_version }}"

Common Use Cases

API Calls from Controller

- hosts: webservers
  tasks:
    - name: Notify monitoring
      uri:
        url: https://monitoring.internal/api/downtime
        method: POST
        body_format: json
        body:
          host: "{{ inventory_hostname }}"
          duration: 30
      delegate_to: localhost

Generate Files Locally

- hosts: localhost
  connection: local
  tasks:
    - template:
        src: inventory.yml.j2
        dest: /tmp/generated-inventory.yml

- copy: content: "{{ report | to_nice_json }}" dest: /tmp/report.json

Run Local Script Before Remote Tasks

- hosts: webservers
  pre_tasks:
    - command: /opt/scripts/pre-deploy-check.sh
      delegate_to: localhost
      run_once: true

tasks: - include_role: { name: deploy }

Wait for Remote Service from Controller

- hosts: webservers
  tasks:
    - service: { name: myapp, state: restarted }
      become: true

- wait_for: host: "{{ ansible_host }}" port: 8080 timeout: 60 delegate_to: localhost

Inventory for Localhost

# Implicit localhost (no inventory needed)
localhost ansible_connection=local

# Or in YAML inventory all: hosts: localhost: ansible_connection: local

connection: local vs delegate_to

| Feature | connection: local | delegate_to: localhost | |---------|-------------------|------------------------| | Scope | Entire play | Single task | | Host context | localhost | Original host's vars | | Use case | All-local plays | Mixed local/remote | | Facts | Local facts | Remote host facts |

FAQ

Why use localhost instead of a remote host?

For API calls, file generation, notifications, and any task that should run on the Ansible controller rather than remote hosts.

Does delegate_to affect variables?

The task runs on localhost but still has access to the original host's variables (facts, hostvars). This is useful for API calls that reference remote host info.

Can I gather facts for localhost?

- setup:
  delegate_to: localhost
  delegate_facts: true  # Store facts under localhost

Run on localhost

- hosts: localhost
  connection: local
  tasks:
    - debug:
        msg: "Running on the Ansible control node"

delegate_to localhost

- hosts: webservers
  tasks:
    - uri:
        url: "http://{{ inventory_hostname }}:8080/health"
      delegate_to: localhost
      register: health

local_action

- hosts: webservers
  tasks:
    - local_action:
        module: copy
        content: "{{ ansible_hostname }}"
        dest: "/tmp/hosts/{{ inventory_hostname }}.txt"

Common Use Cases

API Calls from Controller

- uri:
    url: https://api.example.com/deploy
    method: POST
    body_format: json
    body: { host: "{{ inventory_hostname }}" }
  delegate_to: localhost

Download Then Upload

- get_url:
    url: https://releases.example.com/app.tar.gz
    dest: /tmp/app.tar.gz
  delegate_to: localhost
  run_once: true

- copy: src: /tmp/app.tar.gz dest: /opt/app.tar.gz

Generate Config Locally, Deploy Remotely

- template:
    src: config.j2
    dest: "/tmp/configs/{{ inventory_hostname }}.conf"
  delegate_to: localhost

- copy: src: "/tmp/configs/{{ inventory_hostname }}.conf" dest: /etc/myapp/config.conf become: true

Wait for Service After Deploy

- wait_for:
    host: "{{ inventory_hostname }}"
    port: 8080
    delay: 10
    timeout: 120
  delegate_to: localhost

run_once with delegate_to

# Run once from controller (e.g., send notification)
- uri:
    url: https://hooks.slack.com/services/xxx
    method: POST
    body: '{"text": "Deploy complete"}'
  delegate_to: localhost
  run_once: true

localhost in Inventory

# Explicit localhost
[local]
localhost ansible_connection=local

# Implicit — Ansible adds localhost automatically # You can use it without defining in inventory

FAQ

connection: local vs delegate_to: localhost?

connection: local runs the entire play locally. delegate_to: localhost runs a single task locally while the play targets remote hosts.

Does delegate_to have access to remote facts?

Yes — hostvars[inventory_hostname] still refers to the remote host's facts, even when delegated to localhost.

How to avoid "changed" on localhost tasks?

Use changed_when: false for read-only tasks like API checks or file reads.

Related Articles

Ansible become methods comparedmanaging inventory in Ansible

Category: installation

Watch the video: Ansible localhost & delegate_to: Run Tasks on Control Node Guide — Video Tutorial

Browse all Ansible tutorials · AnsiblePilot Home