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.

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 plugin
• delegate_to: localhost
• local_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 actions • Implicit ‘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 compared • managing inventory in AnsibleCategory: installation
Watch the video: Ansible localhost & delegate_to: Run Tasks on Control Node Guide — Video Tutorial