274

I'm using Ansible for some simple user management tasks with a small group of computers. Currently, I have my playbooks set to hosts: all and my hosts file is just a single group with all machines listed:

# file: hosts
[office]
imac-1.local
imac-2.local
imac-3.local

I've found myself frequently having to target a single machine. The ansible-playbook command can limit plays like this:

ansible-playbook --limit imac-2.local user.yml

But that seems kind of fragile, especially for a potentially destructive playbook. Leaving out the limit flag means the playbook would be run everywhere. Since these tools only get used occasionally, it seems worth taking steps to foolproof playback so we don't accidentally nuke something months from now.
Is there a best practice for limiting playbook runs to a single machine? Ideally the playbooks should be harmless if some important detail was left out.

Kevin C
  • 4,851
  • 8
  • 30
  • 64
joemaller
  • 19,579
  • 7
  • 67
  • 84

14 Answers14

251

Turns out it is possible to enter a host name directly into the playbook, so running the playbook with hosts: imac-2.local will work fine. But it's kind of clunky.

A better solution might be defining the playbook's hosts using a variable, then passing in a specific host address via --extra-vars:

# file: user.yml  (playbook)
---
- hosts: '{{ target }}'
  user: ...

Running the playbook:

ansible-playbook user.yml --extra-vars "target=imac-2.local"

If {{ target }} isn't defined, the playbook does nothing. A group from the hosts file can also be passed through if need be. Overall, this seems like a much safer way to construct a potentially destructive playbook.

Playbook targeting a single host:

$ ansible-playbook user.yml --extra-vars "target=imac-2.local" --list-hosts

playbook: user.yml

  play #1 (imac-2.local): host count=1
    imac-2.local

Playbook with a group of hosts:

$ ansible-playbook user.yml --extra-vars "target=office" --list-hosts

playbook: user.yml

  play #1 (office): host count=3
    imac-1.local
    imac-2.local
    imac-3.local

Forgetting to define hosts is safe!

$ ansible-playbook user.yml --list-hosts

playbook: user.yml

  play #1 ({{target}}): host count=0
Kevin C
  • 4,851
  • 8
  • 30
  • 64
joemaller
  • 19,579
  • 7
  • 67
  • 84
  • 61
    This is solvable in 1.5.3 with `--limit office[0]` – NG. Apr 11 '14 at 18:28
  • 10
    This is a "fail safe" answer, unlike some other ones - if you leave something out, it will do nothing. Running on 'only' one host using Ansible 1.7's ``run_once`` could still be destructive so that's not such a good idea. – RichVel Aug 01 '16 at 16:08
  • 7
    If you'd like a shorter command, `-e` is the equivalent of `--extra-vars` – William Turrell Mar 24 '18 at 11:38
  • 3
    If your ansible configuration requires that hosts cannot be empty or undefined then using a variable combined with a jinja filter works, such as: `hosts: "{{ target | default('no_hosts')}}"` – Zach Weg May 30 '18 at 00:42
  • 1
    You can take the target variable and default filter one step further and add a pattern to restrict the extra_vars input to a specific group, such as webservers: `hosts: "webservers:&{{ target | default('no_hosts')}}"` – Zach Weg May 30 '18 at 00:52
  • I feel like it's very strange to create playbooks that in the end don't represent anything without a massive amount of metadata. At no point - with this setup - you know what is actually on your machines. One of Ansible's strong points is adding your configuration into a versioning system. This solution does none of that. Splitting up your office group in actual meaningful groups feels like a much better approach even though you feel it's clunky. The same accidents you can make with --limit, you have in your -e. I would never suggest this to anyone :S – Viridis Oct 11 '18 at 09:20
  • I used this solution until I discovered the special variable `ansible_limit` which is the contents of the `--limit` CLI option and which can be used instead of the `target` variable in your example. See my answer below. – Manolo Mar 26 '21 at 15:55
210

There's also a cute little trick that lets you specify a single host on the command line (or multiple hosts, I guess), without an intermediary inventory:

ansible-playbook -i "imac1-local," user.yml

Note the comma (,) at the end; this signals that it's a list, not a file.

Now, this won't protect you if you accidentally pass a real inventory file in, so it may not be a good solution to this specific problem. But it's a handy trick to know!

Kevin C
  • 4,851
  • 8
  • 30
  • 64
Tybstar
  • 2,875
  • 1
  • 15
  • 7
  • 3
    That's amazing. I regularly use the -l flag, which works with etc/ansible/hosts (which is populated using the EC2 discovery API), but sometimes I really just need a single machine. Thank you! – Vic May 02 '14 at 02:17
  • 3
    Should this trick make use of the hosts file? I'm using hosts as a dynamic inventory for our AWS EC2 system and it returns: `skipping: no hosts matched`. Perhaps this trick no longer works since `--limit` works? – hamx0r Oct 19 '15 at 23:46
  • 2
    This trick didn't work for me. But this worked: `$ ansible-playbook -kK --limit=myhost1 myplaybook.yml`. See Marwan's answer. – Donn Lee Aug 12 '16 at 21:46
  • 6
    It should be mentioned that for this to work, hosts must be set to `all` in the play(s) - this took me a while to figure out... – Remigius Stalder Apr 24 '17 at 11:36
  • What does `ansible-playbook -i "imac1-local," user.yml` actually mean? I read this as "call `user.yml` using the `imac1-local` inventory, and use whatever hosts `user/yml` specifies". But in the original question, `imac1-local` appears to represent a host/group, but not an inventory. – alex Dec 16 '20 at 21:07
95

This approach will exit if more than a single host is provided by checking the play_hosts variable. The fail module is used to exit if the single host condition is not met. The examples below use a hosts file with two hosts alice and bob.

user.yml (playbook)

---
- hosts: all
  tasks:
    - name: Check for single host
      fail: msg="Single host check failed."
      when: "{{ play_hosts|length }} != 1"

    - debug: msg='I got executed!'

Run playbook with no host filters

$ ansible-playbook user.yml
PLAY [all] ****************************************************************
TASK: [Check for single host] *********************************************
failed: [alice] => {"failed": true}
msg: Single host check failed.
failed: [bob] => {"failed": true}
msg: Single host check failed.
FATAL: all hosts have already failed -- aborting

Run playbook on single host

$ ansible-playbook user.yml --limit=alice

PLAY [all] ****************************************************************

TASK: [Check for single host] *********************************************
skipping: [alice]

TASK: [debug msg='I got executed!'] ***************************************
ok: [alice] => {
    "msg": "I got executed!"
}
Kevin C
  • 4,851
  • 8
  • 30
  • 64
Marwan Alsabbagh
  • 25,364
  • 9
  • 55
  • 65
  • 2
    Definitely the best one, `--limit` is the way to go – berto Jul 18 '16 at 21:18
  • 7
    `play_hosts` is deprecated in Ansible 2.2 and replaced with `ansible_play_hosts`. To run on one host without requiring `--limit`, you can use `when: inventory_hostname == ansible_play_hosts[0]`. – Trevor Robinson Apr 11 '17 at 18:02
  • 2
    `[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ play_hosts|length }} == ''` on Ansible 2.8.4. – Thomas Sep 03 '19 at 08:57
  • 2
    @Thomas -- good call, easy to fix by using `when: ansible_play_hosts|length != 1` – Tyler Sep 25 '20 at 21:35
37

There's IMHO a more convenient way.
You can indeed interactively prompt the user for the machine(s) he wants to apply by using vars_prompt:

---
- hosts: "{{ setupHosts }}"
  vars_prompt:
    - name: "setupHosts"
      prompt: "Which hosts would you like to setup?"
        private: false
   tasks:
     - shell: echo
Kevin C
  • 4,851
  • 8
  • 30
  • 64
Buzut
  • 4,875
  • 4
  • 47
  • 54
25

A slightly different solution is to use the special variable ansible_limit which is the contents of the --limit CLI option for the current execution of Ansible.

- hosts: "{{ ansible_limit | default(omit) }}"

No need to define an extra variable here, just run the playbook with the --limit flag.

ansible-playbook --limit imac-2.local user.yml
Kevin C
  • 4,851
  • 8
  • 30
  • 64
Manolo
  • 826
  • 7
  • 8
18

To expand on joemailer's answer, if you want to have the pattern-matching ability to match any subset of remote machines (just as the ansible command does), but still want to make it very difficult to accidentally run the playbook on all machines, this is what I've come up with:

Same playbook as the in other answer:

# file: user.yml  (playbook)
---
- hosts: '{{ target }}'
  user: ...

Let's have the following hosts:

imac-10.local
imac-11.local
imac-22.local

Now, to run the command on all devices, you have to explicty set the target variable to "all"

ansible-playbook user.yml --extra-vars "target=all"

And to limit it down to a specific pattern, you can set target=pattern_here

or, alternatively, you can leave target=all and append the --limit argument, eg:

--limit imac-1*

ie. ansible-playbook user.yml --extra-vars "target=all" --limit imac-1* --list-hosts

which results in:

playbook: user.yml

  play #1 (office): host count=2
    imac-10.local
    imac-11.local
deadbeef404
  • 623
  • 4
  • 14
  • This is the pattern that I have followed in [ansible-django-postgres-nginx](https://github.com/ajoyoommen/ansible-django-postgres-nginx) – Ajoy Mar 22 '17 at 06:02
11

I really don't understand how all the answers are so complicated, the way to do it is simply:

ansible-playbook user.yml -i hosts/hosts --limit imac-2.local --check

The check mode allows you to run in dry-run mode, without making any change.

knocte
  • 16,941
  • 11
  • 79
  • 125
  • 9
    Likely because wondering about the answers, you missed the question, which asked for a way to prevent from running when parameters are omitted by mistake. You suggested adding more parameters which goes against the requirement. – techraf Nov 16 '17 at 00:48
  • 3
    ah, sure, but if people upvote me, it might be because they are Ansible newbies (like I was when I wrote my answer) which don't even know about the flag `--check`, so I guess this is still useful documentation-wise, as this question may be very *googlable* – knocte Jan 25 '19 at 08:47
6

AWS users using the EC2 External Inventory Script can simply filter by instance id:

ansible-playbook sample-playbook.yml --limit i-c98d5a71 --list-hosts

This works because the inventory script creates default groups.

Frank
  • 1,479
  • 11
  • 15
5

Since version 1.7 ansible has the run_once option. Section also contains some discussion of various other techniques.

Berend de Boer
  • 1,953
  • 20
  • 15
5

We have some generic playbooks that are usable by a large number of teams. We also have environment specific inventory files, that contain multiple group declarations.

To force someone calling a playbook to specify a group to run against, we seed a dummy entry at the top of the playbook:

[ansible-dummy-group]
dummy-server

We then include the following check as a first step in the shared playbook:

- hosts: all
  gather_facts: False
  run_once: true
  tasks:
  - fail:
      msg: "Please specify a group to run this playbook against"
    when: '"dummy-server" in ansible_play_batch'

If the dummy-server shows up in the list of hosts this playbook is scheduled to run against (ansible_play_batch), then the caller didn't specify a group and the playbook execution will fail.

mcdowellstl
  • 139
  • 2
  • 6
  • `ansible_play_batch` lists the current batch only, so when using batching this is still unsafe. It's better to use `ansible_play_hosts` instead. – Thomas Sep 03 '19 at 09:07
  • Apart from that, this trick seems to be the simplest and closest to what was asked; I'm adopting it! – Thomas Sep 03 '19 at 09:13
5

This shows how to run the playbooks on the target server itself.

This is a bit trickier if you want to use a local connection. But this should be OK if you use a variable for the hosts setting and in the hosts file create a special entry for localhost.

In (all) playbooks have the hosts: line set to:

- hosts: "{{ target | default('no_hosts')}}"

In the inventory hosts file add an entry for the localhost which sets the connection to be local:

[localhost]
127.0.0.1  ansible_connection=local

Then on the command line run commands explicitly setting the target - for example:

$ ansible-playbook --extra-vars "target=localhost" test.yml

This will also work when using ansible-pull:

$ ansible-pull -U <git-repo-here> -d ~/ansible --extra-vars "target=localhost" test.yml

If you forget to set the variable on the command line the command will error safely (as long as you've not created a hosts group called 'no_hosts'!) with a warning of:

skipping: no hosts matched

And as mentioned above you can target a single machine (as long as it is in your hosts file) with:

$ ansible-playbook --extra-vars "target=server.domain" test.yml

or a group with something like:

$ ansible-playbook --extra-vars "target=web-servers" test.yml
bailey86
  • 67
  • 1
  • 7
0

I have a wrapper script called provision forces you to choose the target, so I don't have to handle it elsewhere.

For those that are curious, I use ENV vars for options that my vagrantfile uses (adding the corresponding ansible arg for cloud systems) and let the rest of the ansible args pass through. Where I am creating and provisioning more than 10 servers at a time I include an auto retry on failed servers (as long as progress is being made - I found when creating 100 or so servers at a time often a few would fail the first time around).

echo 'Usage: [VAR=value] bin/provision [options] dev|all|TARGET|vagrant'
echo '  bootstrap - Bootstrap servers ssh port and initial security provisioning'
echo '  dev - Provision localhost for development and control'
echo '  TARGET - specify specific host or group of hosts'
echo '  all - provision all servers'
echo '  vagrant - Provision local vagrant machine (environment vars only)'
echo
echo 'Environment VARS'
echo '  BOOTSTRAP - use cloud providers default user settings if set'
echo '  TAGS - if TAGS env variable is set, then only tasks with these tags are run'
echo '  SKIP_TAGS - only run plays and tasks whose tags do not match these values'
echo '  START_AT_TASK - start the playbook at the task matching this name'
echo
ansible-playbook --help | sed -e '1d
    s#=/etc/ansible/hosts# set by bin/provision argument#
    /-k/s/$/ (use for fresh systems)/
    /--tags/s/$/ (use TAGS var instead)/
    /--skip-tags/s/$/ (use SKIP_TAGS var instead)/
    /--start-at-task/s/$/ (use START_AT_TASK var instead)/
'
iheggie
  • 2,011
  • 23
  • 23
0

I think instead of manually passing the host you can use

run_once: true

This will run that particular task only on the single machine I generally use this param while running Django migrations on deployment.

Jay Jain
  • 59
  • 7
-1

I would suggest using --limit <hostname or ip>

Jobin James
  • 916
  • 10
  • 13