151

This is a fragment of a playbook that I'm using (server.yml):

- name: Determine Remote User
  hosts: web
  gather_facts: false
  roles:
    - { role: remote-user, tags: [remote-user, always] }

My hosts file has different groups of servers, e.g.

[web]
x.x.x.x

[droplets]
x.x.x.x

Now I want to execute ansible-playbook -i hosts/<env> server.yml and override hosts: web from server.yml to run this playbook for [droplets].

Can I just override as a one time off thing, without editing server.yml directly?

Thanks.

luqo33
  • 8,001
  • 16
  • 54
  • 107

12 Answers12

179

I don't think Ansible provides this feature, which it should. Here's something that you can do:

hosts: "{{ variable_host | default('web') }}"

and you can pass variable_host from either command-line or from a vars file, e.g.:

ansible-playbook server.yml --extra-vars "variable_host=newtarget(s)"
030
  • 10,842
  • 12
  • 78
  • 123
wallydrag
  • 1,924
  • 1
  • 11
  • 9
  • 5
    A small correction required. It should be `hosts: "{{ variable_host | default('web')}}"` – SPM Oct 21 '15 at 08:58
  • 20
    Here is a note that I feel would complete the answer for ansible newbies searching for this solution: Example: `ansible-playbook server.yml --extra-vars "variable_host=newtarget(s)"` – Frobbit Jan 15 '16 at 19:29
  • 3
    **WHEN** (ie in what order) does ansible parse variables Variables in `group_vars/all` appears to be parsed **after** the `hosts:` line of the playbook. However, variables in `vars:` and variables in `vars_files:` are parsed before the `hosts:` line? **NOTE** I am _not_ asking about precedence. – Felipe Alvarez Jun 29 '17 at 05:23
  • 3
    You can use also `-e` instead of `--extra-vars`. – noun Aug 29 '18 at 22:06
  • 1
    Look at other answers for more details – Anand Varkey Philips Jan 03 '19 at 09:45
  • The method doesn't seem like handling host exclusion. For example: `ansible-playbook server.yml --extra-vars "variable_host=newtarget:!subset"` – weijie lin Mar 08 '19 at 14:27
  • 1
    This method also works with cloud provider group_names, such as AWS tags. `--extra-vars "myhosts=tag_Name_myinstancename"` or `-e "myhosts=us_west_2:&tag_env_production"`. This is super handy if you have a playbook that you want to apply to multiple deployed environments. – aidan Mar 22 '20 at 17:59
  • 1
    I thought that `newtarget(s)` was a function, and `s` a parameter. `newtarget(s)` means the name of a host: _newtarget_ (in singular), or the names of multiple hosts: _newtargets_ (in plural). – robe007 Nov 09 '20 at 16:44
  • 1
    Another solution is to use the special variable `ansible_limit` which is the contents of the `--limit` option for the current execution of Ansible: `hosts: "{{ ansible_limit | default(omit) }}"`. See my answer below. – Manolo Mar 26 '21 at 16:11
  • Was using Ansible 2.9, passing array to host field didn't work. Instead had to use 'add_host' module which gets a group which will be passed on to the 'host' field – Prem Oct 06 '22 at 12:13
73

For anyone who might come looking for the solution.
Play Book

- hosts: '{{ host }}'
  tasks:
  - debug: msg="Host is {{ ansible_fqdn }}"

Inventory

[web]
x.x.x.x

[droplets]
x.x.x.x

Command: ansible-playbook deplyment.yml -i hosts --extra-vars "host=droplets" So you can specify the group name in the extra-vars

Mrunal Gosar
  • 4,595
  • 13
  • 48
  • 71
  • 2
    Note, be careful with var naming. I was testing this using `play_hosts` and not getting the expected results because I forgot that `play_hosts` is an internal Ansible var for all the hosts in the current play. – Ryan Fisher Jul 02 '18 at 17:29
  • I suppose default should be set as in the answer above. –  Sep 12 '19 at 10:51
43

We use a simple fail task to force the user to specify the Ansible limit option, so that we don't execute on all hosts by default/accident.

The easiest way I found is this:

---
- name: Force limit
  # 'all' is okay here, because the fail task will force the user to specify a limit on the command line, using -l or --limit
  hosts: 'all'

  tasks:
  - name: checking limit arg
    fail:
      msg: "you must use -l or --limit - when you really want to use all hosts, use -l 'all'"
    when: ansible_limit is not defined
    run_once: true

Now we must use the -l (= --limit option) when we run the playbook, e.g.

ansible-playbook playbook.yml -l www.example.com

Limit option docs:

Limit to one or more hosts This is required when one wants to run a playbook against a host group, but only against one or more members of that group.

Limit to one host

ansible-playbook playbooks/PLAYBOOK_NAME.yml --limit "host1"

Limit to multiple hosts

ansible-playbook playbooks/PLAYBOOK_NAME.yml --limit "host1,host2"

Negated limit.
NOTE: Single quotes MUST be used to prevent bash interpolation.

ansible-playbook playbooks/PLAYBOOK_NAME.yml --limit 'all:!host1'

Limit to host group

ansible-playbook playbooks/PLAYBOOK_NAME.yml --limit 'group1'

TmTron
  • 17,012
  • 10
  • 94
  • 142
26

This is a bit late, but I think you could use the --limit or -l command to limit the pattern to more specific hosts. (version 2.3.2.0)

You could have - hosts: all (or group) tasks: - some_task

and then ansible-playbook playbook.yml -l some_more_strict_host_or_pattern and use the --list-hosts flag to see on which hosts this configuration would be applied.

Jonathan Hamel
  • 1,393
  • 13
  • 18
  • 3
    I'm very new to ansible but I consider this a very effective solution, much more compact than the others. Why was it downvoted? – Alessandro Dentella Oct 05 '17 at 15:45
  • 23
    This is dangerous. In case one forgets to `limit` the list of hosts affected the playbook may cause a lot of damage. – Alexander Shcheblikin Oct 30 '17 at 11:52
  • 3
    I find that using `--extra-vars "variable_host=newtarget(s)"` just like the accepted solution is as dangerous and a more complicated solution. It uses a default hosts of `web` which could be applied here as well instead of `all`. You could use a strict hosts group as default to avoid making a mistake and use the `--list-hosts` flag to have a clear understanding of which hosts you are affecting. – Jonathan Hamel Oct 30 '17 at 12:59
  • I completely agree, there is nothing worse about this approach than the accepted answer. If you use "all" as default with the accepted approach, then you are in just as much trouble. If you use something other than "all" with this approach you are just as safe. So this is a better solution as it requires less code an uses built in functionality for just this purpose. +1. – Tobb Mar 01 '18 at 14:08
  • 4
    Solution with extra-vars allows to specify empty group (or non-existing) as default value. So if you forget to provide the variable via command line nothing bad happens. Solution with "--limit"option more dangerous because the playbook could not use empty group as default value for the hosts. Option "--llmit" is applied to the hosts value hence it will be applied to the empty groups and will provide empty result. So you HAVE to use "all" or some other non-empty host as default value. And some day you will forget to provide "--limit" argument and playbook will be applied to all hosts. – Stack Exchange User Mar 30 '18 at 11:18
  • I think arguments are valid for both sides. In production, this method is a bit risky where in the accepted solution a 'null' group can be used for saftey. But in testing and development I think your solution is superior. imho both solutions should be accepted since the requester didn't specify if this is for testing or production. – Leo Ufimtsev Sep 28 '18 at 01:28
  • @sudosoul the `limit` flag will to an intersection of the given hosts and the limit pattern. If `host: undefined`, no hosts will ever be used. – Jonathan Hamel Jul 24 '19 at 19:10
  • @JonathanHamel Yeah just realized that. Check out the answer though I just posted, I think it solves the issue of using limit. – sudo soul Jul 24 '19 at 22:26
  • 4
    This should be combined with @TmTron's answer to catch the case where the caller failed to supply `--limit` (otherwise it will affect all possible hosts, which may not be the behaviour you want) – ncoghlan Aug 28 '19 at 07:11
19

An other 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) }}"

If the --limit option is omitted, then Ansible issues a warning, but does nothing since no host matched.

[WARNING]: Could not match supplied host pattern, ignoring: None

PLAY ****************************************************************
skipping: no hosts matched
Manolo
  • 826
  • 7
  • 8
10

I'm using another approach that doesn't need any inventory and works with this simple command:

ansible-playbook site.yml -e working_host=myhost

To perform that, you need a playbook with two plays:

  • first play runs on localhost and add a host (from given variable) in a known group in inmemory inventory
  • second play runs on this known group

A working example (copy it and runs it with previous command):

- hosts: localhost
  connection: local
  tasks:
  - add_host:
      name: "{{ working_host }}"
      groups: working_group
    changed_when: false

- hosts: working_group
  gather_facts: false
  tasks:
  - debug:
      msg: "I'm on {{ ansible_host }}"

I'm using ansible 2.4.3 and 2.3.3

Nelson G.
  • 5,145
  • 4
  • 43
  • 54
9

I changed mine to default to no host and have a check to catch it. That way the user or cron is forced to provide a single host or group etc. I like the logic from the comment from @wallydrag. The empty_group contains no hosts in the inventory.

- hosts: "{{ variable_host | default('empty_group') }}"

Then add the check in tasks:

   tasks:
   - name: Fail script if required variable_host parameter is missing
     fail:
       msg: "You have to add the --extra-vars='variable_host='"
     when: (variable_host is not defined) or (variable_host == "")
Tony-Caffe
  • 631
  • 1
  • 8
  • 13
6

Just came across this googling for a solution. Actually, there is one in Ansible 2.5. You can specify your inventory file with --inventory, like this: ansible --inventory configs/hosts --list-hosts all

Bebef
  • 141
  • 2
  • 6
  • I believe this is the most correct answer in the Year of Our Lord 2019. From Ansible 2.8.4's -h: `-i INVENTORY, --inventory=INVENTORY, --inventory-file=INVENTORY specify inventory host path or comma separated host list. --inventory-file is deprecated` – pyansharp Aug 18 '19 at 06:17
3

If you want to run a task that's associated with a host, but on different host, you should try delegate_to.

In your case, you should delegate to your localhost (ansible master) and calling ansible-playbook command

nghnam
  • 661
  • 7
  • 10
3

I am using ansible 2.5 (2.5.3 exactly), and it seems that the vars file is loaded before the hosts param is executed. So you can set the host in a vars.yml file and just write hosts: {{ host_var }} in your playbook

For example, in my playbook.yml:

---
- hosts: "{{ host_name }}"
  become: yes
  vars_files:
    - vars/project.yml
  tasks:
    ... 

And inside vars/project.yml:

---

# general
host_name: your-fancy-host-name
LightMan
  • 3,517
  • 31
  • 31
0

Here's a cool solution I came up to safely specify hosts via the --limit option. In this example, the play will end if the playbook was executed without any hosts specified via the --limit option.

This was tested on Ansible version 2.7.10

---
- name: Playbook will fail if hosts not specified via --limit option.
  # Hosts must be set via limit. 
  hosts: "{{ play_hosts }}"
  connection: local
  gather_facts: false
  tasks:
  - set_fact:
      inventory_hosts: []
  - set_fact:
      inventory_hosts: "{{inventory_hosts + [item]}}"
    with_items: "{{hostvars.keys()|list}}"

  - meta: end_play
    when: "(play_hosts|length) == (inventory_hosts|length)"

  - debug:
      msg: "About to execute tasks/roles for {{inventory_hostname}}"
sudo soul
  • 1,504
  • 13
  • 20
0

This worked for me as I am using Azure devops to deploy an application using CICD pipelines. I had to make this hosts (in yml file) more dynamic so in release pipeline I can add it's value, for example:

--extra-vars "host=$(target_host)"

pipeline_variable

My ansible playbook looks like this

- name: Apply configuration to test nodes
  hosts: '{{ host }}'