The Problem

Your playbook uses local_action to run a task on the Ansible control node, but it fails:

bash
fatal: [server]: FAILED! => {"msg": "Failed to connect to the host via ssh: ssh: connect to host localhost port 22: Connection refused"}

Or:

bash
fatal: [server]: FAILED! => {"msg": "Invalid data passed to 'local_action', it requires a dictionary, got a string"}

Or the task runs on the remote host instead of locally.

Why This Happens

local_action errors stem from:

SSH connection to localhost - Ansible tries SSH instead of local execution.

Wrong syntax format - Using incorrect local_action syntax (old vs new).

Missing local connection config - localhost not configured for local connection.

Environment variable issues - Variables not available in local context.

Permission issues - Running as wrong user locally.

Understanding local_action

local_action runs a task on the Ansible control node (where ansible-playbook runs) instead of the target host:

yaml
- hosts: webservers
  tasks:
    - name: Run locally
      local_action: command whoami
      # Runs on control node, not webserver

Modern equivalent using delegate_to:

yaml
- name: Run locally
  command: whoami
  delegate_to: localhost
  connection: local

Diagnosing the Issue

Check localhost configuration:

bash
ansible localhost -m ping
ansible localhost -m setup

Check connection type:

bash
ansible-config dump | grep connection

Debug local execution:

yaml
- name: Debug local action
  local_action: command hostname
  register: local_host
- debug:
    msg: "Ran on: {{ local_host.stdout }}"

The Fix

Fix 1: Configure localhost for Local Connection

Add localhost to inventory with local connection:

yaml
# inventory.yml
all:
  hosts:
    localhost:
      ansible_connection: local

Or in ansible.cfg:

ini
[defaults]
# Default connection for localhost
localhost_connection = local

Fix 2: Use Correct local_action Syntax

Old vs new syntax:

```yaml # OLD SYNTAX (still works) - name: Old style local action local_action: command hostname

# NEW SYNTAX (recommended) - name: New style local action command: hostname delegate_to: localhost connection: local ```

For module with arguments:

```yaml # OLD SYNTAX - can be confusing - name: Old style with args local_action: module: copy src: files/config.conf dest: /tmp/config.conf

# NEW SYNTAX - clearer - name: New style with args copy: src: files/config.conf dest: /tmp/config.conf delegate_to: localhost connection: local ```

Fix 3: Fix SSH Connection Errors

If Ansible tries SSH to localhost:

```yaml # WRONG - SSH connection fails - hosts: webservers tasks: - name: Local task without connection command: hostname delegate_to: localhost # Ansible may try SSH to localhost

# CORRECT - explicit local connection - hosts: webservers tasks: - name: Local task with local connection command: hostname delegate_to: localhost connection: local ```

Fix 4: Handle Variable Context

Variables in local context may differ:

```yaml - hosts: webservers tasks: - name: Use inventory_hostname locally debug: msg: "Processing {{ inventory_hostname }}" delegate_to: localhost connection: local # inventory_hostname is still webserver, not localhost

  • name: Use local facts
  • debug:
  • msg: "{{ ansible_facts }}"
  • delegate_to: localhost
  • connection: local
  • # ansible_facts are from localhost, not webserver
  • `

Gather facts locally if needed:

```yaml - hosts: webservers tasks: - name: Gather localhost facts setup: delegate_to: localhost delegate_facts: yes connection: local

  • name: Use localhost facts
  • debug:
  • msg: "{{ hostvars['localhost']['ansible_facts'] }}"
  • `

Fix 5: Common local_action Use Cases

Git operations on control node: ```yaml - hosts: webservers tasks: - name: Clone repo locally git: repo: https://github.com/example/repo.git dest: /tmp/repo delegate_to: localhost connection: local

  • name: Copy to remote
  • synchronize:
  • src: /tmp/repo/
  • dest: /opt/app/
  • `

API calls from control node: ``yaml - hosts: webservers tasks: - name: Register with API uri: url: https://api.example.com/register method: POST body: host: "{{ inventory_hostname }}" delegate_to: localhost connection: local

File operations locally: ```yaml - hosts: webservers tasks: - name: Generate config locally template: src: templates/app.conf.j2 dest: /tmp/app-{{ inventory_hostname }}.conf delegate_to: localhost connection: local

  • name: Upload to server
  • copy:
  • src: /tmp/app-{{ inventory_hostname }}.conf
  • dest: /etc/app/app.conf
  • `

Fix 6: Handle Permission Issues

Run local tasks as correct user:

yaml
- name: Local task as specific user
  command: whoami
  delegate_to: localhost
  connection: local
  become: yes
  become_user: root

Or use ansible_user for localhost:

yaml
# inventory.yml
all:
  hosts:
    localhost:
      ansible_connection: local
      ansible_user: "{{ lookup('env', 'USER') }}"

Fix 7: local_action with run_once

Combine local_action with run_once:

yaml
- hosts: webservers
  tasks:
    - name: One-time local setup
      command: /opt/prepare.sh
      delegate_to: localhost
      connection: local
      run_once: yes
      # Runs once locally, not per webserver

Verifying the Fix

Test local_action:

```yaml # test_local.yml - hosts: localhost connection: local gather_facts: no tasks: - name: Test local action command: hostname register: result

  • name: Verify
  • debug:
  • msg: "Hostname: {{ result.stdout }}"
  • `

Run:

bash
ansible-playbook test_local.yml -v

Expected: `` TASK [Test local action] *********************************************************** changed: [localhost] TASK [Verify] *********************************************************** ok: [localhost] => {"msg": "Hostname: ansible-control-node"}

Prevention

Add localhost to inventory always:

```yaml # inventory.yml all: hosts: localhost: ansible_connection: local

webservers: hosts: web1: web2: ```

Use explicit connection:

yaml
- name: Any local task
  command: something
  delegate_to: localhost
  connection: local  # Always specify