Safely limiting Ansible playbooks to a single mach

2020-05-10 16:53发布

问题:

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.

回答1:

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


回答2:

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!



回答3:

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!"
}


回答4:

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

---

- hosts: "{{ setupHosts }}"
  vars_prompt:
    - name: "setupHosts"
      prompt: "Which hosts would you like to setup?"
      private: no
  tasks:
    […]


回答5:

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


回答6:

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.



回答7:

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.



回答8:

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.



回答9:

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



回答10:

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


回答11:

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)/
'