Ansible Local Connection — Run Tasks on the Controller

Introduction

By default, Ansible connects to remote hosts via SSH. connection: local runs tasks directly on the Ansible controller — no SSH, no remote host needed. Use it for API calls, cloud provisioning, local file manipulation, and any task that should execute on the machine running the playbook.

Three Ways to Run Locally

1. Play-Level Connection

---
- name: Run everything locally
  hosts: localhost
  connection: local
  gather_facts: true

  tasks:
    - name: Create a local directory
      ansible.builtin.file:
        path: /tmp/ansible-output
        state: directory

    - name: Call an API
      ansible.builtin.uri:
        url: "https://api.example.com/health"

2. delegate_to localhost

- name: Deploy to servers
  hosts: webservers
  tasks:
    - name: Remove from load balancer (run on controller)
      ansible.builtin.uri:
        url: "https://lb.example.com/api/remove"
        method: POST
        body: '{"host": "{{ inventory_hostname }}"}'
      delegate_to: localhost

    - name: Deploy application (run on remote host)
      ansible.builtin.command:
        cmd: /opt/deploy.sh

3. Per-Task Connection Override

    - name: Generate report locally
      ansible.builtin.template:
        src: report.j2
        dest: "/tmp/report-{{ inventory_hostname }}.html"
      connection: local

Common Use Cases

Cloud Provisioning

- name: Provision AWS infrastructure
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Create VPC
      amazon.aws.ec2_vpc_net:
        name: production
        cidr_block: 10.0.0.0/16
        state: present
      register: vpc

    - name: Create security group
      amazon.aws.ec2_security_group:
        name: web-sg
        description: Web server security group
        vpc_id: "{{ vpc.vpc.id }}"
        rules:
          - proto: tcp
            ports: [80, 443]
            cidr_ip: 0.0.0.0/0

    - name: Launch instances
      amazon.aws.ec2_instance:
        name: "web-{{ item }}"
        instance_type: t3.micro
        state: running
      loop: [1, 2, 3]

API Orchestration

- name: Orchestrate deployment
  hosts: localhost
  connection: local
  tasks:
    - name: Start deployment in CI/CD
      ansible.builtin.uri:
        url: "https://ci.example.com/api/deploy"
        method: POST
        headers:
          Authorization: "Bearer {{ ci_token }}"
        body_format: json
        body:
          version: "{{ app_version }}"
      no_log: true

    - name: Notify Slack
      ansible.builtin.uri:
        url: "{{ slack_webhook }}"
        method: POST
        body_format: json
        body:
          text: "Deployment of v{{ app_version }} started"

Local File Processing

- name: Process local data
  hosts: localhost
  connection: local
  tasks:
    - name: Read CSV and create inventory
      ansible.builtin.read_csv:
        path: servers.csv
      register: server_list

    - name: Generate configs from template
      ansible.builtin.template:
        src: config.j2
        dest: "/tmp/configs/{{ item.hostname }}.conf"
      loop: "{{ server_list.list }}"

Mixed Local and Remote

- name: Deploy with local and remote tasks
  hosts: webservers
  tasks:
    # Runs on controller
    - name: Download artifact
      ansible.builtin.get_url:
        url: "https://releases.example.com/app-{{ version }}.tar.gz"
        dest: /tmp/app.tar.gz
      delegate_to: localhost
      run_once: true

    # Runs on each remote host
    - name: Upload artifact
      ansible.builtin.copy:
        src: /tmp/app.tar.gz
        dest: /opt/app/

    # Runs on each remote host
    - name: Extract and deploy
      ansible.builtin.unarchive:
        src: /opt/app/app.tar.gz
        dest: /opt/app/
        remote_src: true

    # Runs on controller
    - name: Update deployment tracker
      ansible.builtin.uri:
        url: "https://deploy.example.com/api/status"
        method: PUT
        body: '{"host": "{{ inventory_hostname }}", "status": "deployed"}'
      delegate_to: localhost

localhost in Inventory

# Implicit localhost (always available, uses local connection)
# No need to define — Ansible creates it automatically

# Explicit localhost with specific settings
[local]
localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3

connection: local vs delegate_to

Featureconnection: localdelegate_to: localhost
ScopeEntire playSingle task
inventory_hostnamelocalhostOriginal remote host
hostvarslocalhost's varsRemote host's vars
Use caseEntire play runs locallyOne task runs locally
# With delegate_to, you still have remote host context:
- hosts: webservers
  tasks:
    - name: Register in DNS
      community.general.nsupdate:
        server: ns1.example.com
        zone: example.com
        record: "{{ inventory_hostname }}"  # ← webserver name
        value: "{{ ansible_host }}"         # ← webserver IP
      delegate_to: localhost
      # Runs on controller but uses webserver's variables

Troubleshooting

IssueSolution
"localhost not in inventory"Use hosts: localhost — it's implicit
Wrong Python interpreterSet ansible_python_interpreter: /usr/bin/python3
Facts from wrong hostdelegate_to keeps original host facts; connection: local uses localhost facts
SSH connection attemptedEnsure connection: local or ansible_connection=local
Module not found locallyInstall required Python packages on the controller

Best Practices

  1. Use connection: local for API/cloud plays — entire play runs locally
  2. Use delegate_to: localhost for mixed plays — one task locally, rest remote
  3. Set gather_facts: false — unless you need controller's system facts
  4. Use run_once: true with delegate_to — avoid duplicate API calls
  5. Keep secrets local — API tokens stay on controller, never sent to remote hosts
  6. ansible_python_interpreter — set explicitly to avoid Python 2/3 issues

Conclusion

Local connection is how Ansible manages things that don't need SSH — cloud APIs, CI/CD orchestration, file processing, and deployment tracking. Use connection: local when the entire play runs on the controller, and delegate_to: localhost when individual tasks within a remote play need to run locally. Together they let you mix infrastructure provisioning, application deployment, and API orchestration in a single playbook.