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
| Feature | connection: local | delegate_to: localhost |
|---|---|---|
| Scope | Entire play | Single task |
inventory_hostname | localhost | Original remote host |
hostvars | localhost's vars | Remote host's vars |
| Use case | Entire play runs locally | One 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
| Issue | Solution |
|---|---|
| "localhost not in inventory" | Use hosts: localhost — it's implicit |
| Wrong Python interpreter | Set ansible_python_interpreter: /usr/bin/python3 |
| Facts from wrong host | delegate_to keeps original host facts; connection: local uses localhost facts |
| SSH connection attempted | Ensure connection: local or ansible_connection=local |
| Module not found locally | Install required Python packages on the controller |
Best Practices
- Use
connection: localfor API/cloud plays — entire play runs locally - Use
delegate_to: localhostfor mixed plays — one task locally, rest remote - Set
gather_facts: false— unless you need controller's system facts - Use
run_once: truewithdelegate_to— avoid duplicate API calls - Keep secrets local — API tokens stay on controller, never sent to remote hosts
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.