How to Run an Ansible Playbook Locally Without SSH: 5 Proven Methods

Use the connection: local keyword in your play, target the pre-configured localhost host, or pass -c local on the CLI to execute Ansible tasks directly on the control node without SSH overhead.

Running an Ansible playbook locally is essential for controller provisioning, CI/CD pipelines, and testing roles without remote infrastructure. While Ansible is architected for remote execution via SSH, the ansible/ansible repository provides a robust local connection plugin that bypasses network transport entirely. This article explores the five most reliable methods to run ansible playbook locally, backed by specific source file references from the codebase.

Understanding the Local Connection Plugin Architecture

The core mechanism for local execution resides in lib/ansible/plugins/connection/local.py. This plugin implements the ConnectionBase API and executes commands using subprocess.Popen directly on the controller node rather than opening an SSH session.

Key implementation details from the source:

  • User context: The plugin sets self._play_context.remote_user to the current controller user via getpass.getuser() because no remote authentication is required.
  • File transfers: Instead of SCP or SFTP, put_file and fetch_file use shutil.copyfile since both source and destination exist on the same filesystem.
  • Privilege escalation: The exec_command method allocates a pseudo-TTY via pty.openpty() when become is enabled, ensuring sudo password prompts work correctly.

Method 1: Configure the Play with connection: local

Setting connection: local at the play level forces all tasks in that play to execute on the controller. This is the most explicit method and documents intent clearly within your playbook.

---
- name: Configure the Ansible controller
  hosts: localhost
  connection: local
  become: true
  tasks:
    - name: Install required packages
      ansible.builtin.apt:
        name: git
        state: present

    - name: Create application directory
      ansible.builtin.file:
        path: /opt/myapp
        state: directory
        mode: '0755'

This approach is ideal for controller provisioning, bootstrap scripts, or any workflow where the target host is the machine running Ansible.

Method 2: Target the Implicit localhost Host

Ansible automatically defines a localhost host in the default inventory located at lib/ansible/inventory/source/local.py. This host entry pre-configures ansible_connection=local, meaning you can target localhost without any additional connection parameters.

ansible-playbook -i localhost, site.yml

Note: The trailing comma after localhost is required to indicate a static inventory entry rather than a file path.

This method leverages the built-in inventory logic, making it perfect for CI/CD pipelines where you want minimal configuration syntax.

Method 3: Override with the -c local CLI Flag

For ad-hoc execution or testing scenarios where you cannot modify the playbook, the -c (or --connection) CLI flag forces a specific connection plugin. This option is parsed in lib/ansible/cli/playbook.py and injected into the play context.

ansible-playbook -i inventory.ini -c local site.yml

This approach applies the local connection to all hosts in the run, overriding any connection keywords defined in the playbook. Use this for rapid testing or when executing playbooks designed for remote hosts against the controller temporarily.

Method 4: Delegate Specific Tasks with delegate_to: localhost

When most tasks should run on remote hosts but specific operations (like generating reports or rendering templates) must execute on the controller, use delegate_to: localhost. This task-level directive forces the local connection plugin for that specific task while maintaining the remote context for others.

- name: Deploy application to remote servers
  hosts: webservers
  become: true
  tasks:
    - name: Generate configuration on controller
      ansible.builtin.template:
        src: web.conf.j2
        dest: /tmp/web.conf
      delegate_to: localhost

    - name: Copy configuration to remote host
      ansible.builtin.copy:
        src: /tmp/web.conf
        dest: /etc/web.conf

The delegate_to mechanism is implemented in the task executor, which looks up the delegated host's connection settings—identical to how it handles the implicit localhost entry.

Method 5: Set ansible_connection=local in Inventory

For environments where multiple hosts or groups require local execution, define the connection type in your inventory file or group variables. Setting ansible_connection=local as a host or group variable ensures the local plugin is used without modifying playbooks or CLI commands.

[local]
mycontroller ansible_connection=local ansible_user=admin

[local:vars]
ansible_connection=local

This declarative approach centralizes connection logic in your inventory, making it ideal for mixed environments where some managed nodes are actually the controller itself.

How Local Execution Handles Privilege Escalation and File Transfers

Understanding the internal mechanics helps troubleshoot edge cases. According to the source code in lib/ansible/plugins/connection/local.py:

  • Privilege escalation: When become: true is set, the plugin allocates a pseudo-terminal using pty.openpty() to handle interactive sudo password prompts. The exec_command method manages this allocation before spawning the subprocess.

  • File transfers: Unlike remote connections that use SCP/SFTP, the local plugin implements put_file and fetch_file using shutil.copyfile. This is possible because both source and destination paths reside on the same filesystem, eliminating network overhead entirely.

These implementations ensure that running ansible playbook locally provides the same functional capabilities as remote execution, including proper permission handling and idempotent file operations.

Summary

  • The local connection plugin (lib/ansible/plugins/connection/local.py) is the core mechanism that enables running ansible playbook locally without SSH.
  • Five reliable methods exist: connection: local in plays, targeting implicit localhost, using -c local CLI flag, delegate_to: localhost for specific tasks, and setting ansible_connection=local in inventory.
  • Built-in support: The default inventory automatically configures localhost with ansible_connection=local, making local execution seamless for CI/CD pipelines.
  • Full feature parity: Local execution supports privilege escalation (become) and file transfers using shutil.copyfile, ensuring identical behavior to remote connections.

Frequently Asked Questions

What is the difference between connection: local and delegate_to: localhost?

Use connection: local when you want the entire play to execute on the controller node, typically set at the play level alongside hosts: localhost. Use delegate_to: localhost when you need only a specific task to run locally while the rest of the play executes on remote hosts. Both methods invoke the same local connection plugin, but they operate at different scopes.

Does running Ansible locally support sudo and privilege escalation?

Yes. The local connection plugin fully supports privilege escalation via become. When become: true is set, the plugin allocates a pseudo-terminal using pty.openpty() to handle interactive password prompts, then executes the command with elevated privileges via subprocess.Popen. This behavior is implemented in lib/ansible/plugins/connection/local.py.

Why does ansible-playbook -i localhost, site.yml work without specifying -c local?

The default inventory provided by Ansible (defined in lib/ansible/inventory/source/local.py) automatically creates a localhost host entry with ansible_connection=local pre-configured. When you target localhost or 127.0.0.1, Ansible automatically selects the local connection plugin without requiring explicit CLI flags or playbook keywords.

Are file transfers slower when running Ansible locally?

No. Local file transfers are actually faster because they bypass network protocols entirely. The local connection plugin implements put_file and fetch_file using shutil.copyfile, which performs direct filesystem copies since both source and destination reside on the same controller node. This eliminates the overhead of SCP or SFTP encryption and network latency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →