1. Overview

Ansible is a powerful automation tool. It mainly focuses on running tasks on remote hosts. However, in some scenarios, we require running commands on the Ansible controller itself.

Basically, the controller is the system running Ansible. We also refer to this machine as localhost in this tutorial.

In this tutorial, we look at different ways of running commands on the controller. To move on with this tutorial, we need to set up a basic lab.

2. Use Cases for Running Commands Locally

Running commands on the Ansible controller has several common and important purposes:

  • creating a directory to store backups
  • downloading a configuration template
  • running a local shell script, for example, a pre-deployment script

These are some examples of running commands on the Ansible controller host. Yet, there can be specific scenarios that require additional commands to run.

3. Running Commands Locally

Ansible has several ways to run commands directly on the controller host:

  • the connection keyword
  • the local_action module
  • the delegate_to keyword
  • using Ansible ad-hoc commands
  • using Ansible Inventory

Let’s go through each method one by one.

4. Using a connection Plugin

Ansible has many connection plugins. One such plugin is ansible.builtin.local. This plugin also has a short name: local. Basically, it runs tasks on the controller machine.

Moreover, the playbook usually executes quickly as there is no SSH overload.

4.1. Connection Details Inside Playbook

Let’s see how we can set the connection type inside a playbook.

To run a command locally, we set the connection keyword to local. Further, we can add this configuration to a whole playbook or a single task.

Let’s take an example playbook of getting uptime from localhost:

$ cat connection.yaml
---
- name: Get system uptime
  hosts: localhost
  connection: local
  tasks:
    - name: Print uptime
      command: uptime
      register: "output"
    - debug: var=output.stdout

We can break down the above playbook:

  • hosts: localhost runs the tasks on the controller
  • connection: local defines the connection method as local for local execution on the controller
  • command: uptime runs the uptime command on the controller host
  • register: “output” saves the output of the uptime command and stores it in a variable named output
  • debug: var=output.stdout uses the debug module to show the saved output

When we run the above playbook, we see the system uptime in the output:

$ ansible-playbook connection.yaml 
[WARNING]: provided hosts list is empty,...
PLAY [Get system uptime] *************************************
TASK [Gathering Facts] ***************************************
ok...
TASK [Print uptime] ******************************************
changed: [localhost]
TASK [debug] *************************************************
ok: [localhost] => {
    "output.stdout": " 07:06:39 up 16 min,  1 user,  load average: 0.19, 0.06, 0.03"
}
PLAY RECAP ***************************************************
...

In this case, we don’t use any inventory files with the above playbook.

4.2. Connection Details on Command-Line

As another option, we can skip the connection: local line from the playbook. Instead, we can use the –connection=local parameter on the command-line:

$ ansible-playbook connection.yaml --connection=local
[WARNING]: provided hosts list is empty...
PLAY [Get system uptime] *************************************
TASK [Gathering Facts] ***************************************
ok:...
TASK [Print uptime] ******************************************
changed...
TASK [debug] *************************************************
ok: [localhost] => {
    "output.stdout": " 06:56:31 up  3:29,  2 users,  load average: 0.00, 0.00, 0.00"
}
PLAY RECAP ****************************************************
...

As a result, the above playbook runs on the localhost system. Again, it just shows the system uptime using the debug module.

Markedly, there is an implicit localhost warning in the above two cases. We can define a localhost entry in the inventory file to remove this warning. Furthermore, we supply the inventory file name while running the playbook.

5. Using Ansible Inventory

The Ansible inventory file can be configured to define what localhost points to. Consequently, this enables us to target the controller.

Let’s modify the  inventory file to contain an entry for localhost:

$ cat inventory.ini
controller ansible_connection=local

In the above file, controller* is assigned as the name for *localhost.

Then, we add a new playbook, inventory_demo.yaml:

$ cat inventory_demo.yaml
---
- name: Get system uptime
  hosts: controller
  tasks:
    - name: Print uptime
      command: uptime
      register: "output"
    - debug: var=output.stdout

Since we’ve already defined the connection inside the inventory file, we’ve not added any connection directives to the playbook.

Let’s run the above playbook:

$ ansible-playbook inventory_demo.yaml -i inventory.ini
PLAY [Get system uptime] *************************************
TASK [Gathering Facts] ***************************************
ok:...
TASK [Print uptime] *******************************************
changed:...
TASK [debug] *******************************************************************
ok: [localhost] => {
    "output.stdout": " 11:00:30 up 25 min,  1 user,  load average: 0.16, 0.03, 0.01"
}
PLAY RECAP *********************************************************************
...

As a result, we can see the Ansible controller uptime.

6. Using delegate_to Keyword

The delegate_to keyword enables delegating tasks to a specific host, including the controller. This is particularly useful in scenarios where a task needs to be performed on one host but requires context or reference from other hosts.

Delegation is frequently used in managing nodes in a load-balanced pool.

For example, we might want to update a configuration file on all servers. Usually, taking them all down at once isn’t an option. In such cases, by using delegate_to, we can tell Ansible to update the files on each server one by one. At the same time, the load balancer distributes traffic to the other online servers.

Let’s take the example of copying a file to and from localhost:

$ cat delegate_to.yaml
---
- name: copy a file on localhost
  hosts: all
  tasks:
    - name: Copy a file locally
      copy:
        src: /home/vagrant/abc.txt
        dest: /home/vagrant/ansible/
      delegate_to: localhost
      register: copy_result
    - name: Display copy result
      debug:
        var: copy_result

Now, we can break down the above playbook:

  • hosts: all sets all hosts defined in the inventory file as targets for the play
  • copy… this module copies files from the source src to the destination dest
  • delegate_to: localhost delegates the execution of the task to localhost
  • register: copy_result saves the result of the copy operation and stores it in a variable named copy_result
  • var: copy_result sets the variable copy_result containing the outcome of the copy operation

When we run the playbook, the results are shown for each host. However, in PLAY RECAP, we see the changes only on localhost:

$ ansible-playbook delegate_to.yaml -i inventory.ini
...
TASK [Display copy result] *****************************************************************************
ok: [controller] => {
"copy_result": {
"changed": false,
...
PLAY RECAP *********************************************************************
127.0.0.1                  : ok=3    changed=1...
192.168.29.21              : ok=3    changed=0...
...

Thus, the file transfer is done only on localhost.

7. Using the local_action Module

Ansible offers a simpler way to run tasks directly on the controller host. This is done using the local_action module. Moreover, this removes the need for the more verbose delegate_to option.

Let’s again write a playbook to see how the local_action module works. This playbook copies a file to and from localhost:

$ cat local_action.yaml
---
- name: copy a file using local_action
  hosts: all
  tasks:
    - name: Print uptime
      command: uptime
      register: "uptime_result"
    - debug: var=uptime_result.stdout
    - name: Copy a file locally
      local_action: copy src=/home/vagrant/abc.txt dest=/home/vagrant/ansible/
      register: "copy_result"
    - name: Display copy result
      debug:
        var: copy_result
      when: inventory_hostname == 'localhost'

The above playbook is the same as the previous one, with some exceptions:

  • command: uptime tells Ansible to run the uptime command on each targeted host in the inventory
  • register: “uptime_result” stores the result from the uptime command
  • debug: var=uptime_result.stdout shows the saved uptime information for each host
  • local_action… copies files from the source src to the destination dest

Next, we run the above playbook:

$ ansible-playbook local_action.yaml -i inventory.ini
...
TASK [Print uptime] ************************************************************
changed: [localhost]
changed: [192.168.29.21]
changed: [192.168.29.22]
...
TASK [Display copy result] *****************************************************
ok: [localhost] => {
    "copy_result": {
        "changed": false,
...
skipping: [192.168.29.22]
skipping: [192.168.29.21]
...

Notably, this time the uptime command runs on all hosts. However, the file transfers only on localhost.

8. Using Ansible Ad-hoc Commands

We can also run Ansible ad-hoc commands against localhost. To do this, we can use localhost by its name.

Let’s use the shell module to print a message, abc abc:

$ ansible localhost -c local -m shell -a 'echo abc abc'
localhost | CHANGED | rc=0 >>
abc abc

We can break down each part of the command:

  • localhost sets the target host for the task
  • -c local defines the connection method as local
  • -m shell sets the command up to use the shell module
  • a runs a module command

As a matter of fact, we can also use the IP address of localhost:

$ ansible 127.0.0.1 -c local -m shell -a 'echo abc abc' 
127.0.0.1 | CHANGED | rc=0 >> abc abc

Again, the inventory file can rename localhost. For example, we can again refer to 127.0.0.1 as controller in the inventory file:

$ cat inventory.ini 
[controller]
127.0.0.1

Then, we use the Ansible ad-hoc command with name controller:

$ ansible controller -c local -m shell -a 'echo abc abc' -i inventory.ini
127.0.0.1 | CHANGED | rc=0 >>
abc abc

Here, the option -i tells which hosts to select from the inventory file. In the same way, as expected, we can use localhost as the hostname.

As a result, the above command runs the shell command echo abc abc.

9. Conclusion

In this article, we saw how to run a command on Ansible local system, localhost.

First, we used the local connection method. Then, we used the Ansible inventory file. Further, we worked with the delegate_to keyword and the local_action module.

Finally, we saw how to use the ad-hoc commands for running commands on localhost. With these methods, managing both local and remote tasks usually becomes more effective.