Question about YML inventory structure

I’m having trouble wrapping my head around how to accomplish something with an inventory. I have multiple datacetners that internally have similar structures that I want to represent.

What I’d like to be able to do is be able to address “all items in DC1” with a typical --limit dc1 (this does not work as written). What I end up with when trying to do that limit is still all hosts. What I believe is happening is that because the group names match, and dev exists in both groups, it loads both dev groups, and then the dc2 dev group vars are loaded second, and override the DC1 vars.

Is there a way to structure this to achieve what I’m looking for?

---
all:
  vars:
    service1_user: "{{ lookup('env', 'SERVICE1_USER') }}"
    service1_password: "{{ lookup('env','SERVICE1_PASSWORD') }}"
    service2_password: "{{ lookup('env','SERVICE2_PASSWORD') }}"
    service3_user: "{{ lookup('env', 'SERVICE3_USER') }}"
    service3_pass: "{{ lookup('env', 'SERVICE3_PASS') }}"

dc1:
  vars:
    var0: dc1 value
  children:
    dev:
      vars:
        var1: DC1 dev value
        var2: DC1 dev value
        var3: DC1 dev value
      hosts:
        dc1devhost1
        dc1devhost2
        dc1devhost3
        dc1devhost4
    uat:
      vars:
        var1: DC1 uat value
        var2: DC1 uat value
        var3: DC1 uat value
      hosts:
        dc1uathost1
        dc1uathost2
        dc1uathost3
        dc1uathost4
    prod:
      vars:
        var1: DC1 prod value
        var2: DC1 prod value
        var3: DC1 prod value
      hosts:
        dc1prodhost1
        dc1prodhost2
        dc1prodhost3
        dc1prodhost4

dc2:
  vars:
    var0: dc2 value
  children:
    dev:
      vars:
        var1: dc2 dev value
        var2: dc2 dev value
        var3: dc2 dev value
      hosts:
        dc2devhost1
        dc2devhost2
        dc2devhost3
        dc2devhost4
    uat:
      vars:
        var1: dc2 uat value
        var2: dc2 uat value
        var3: dc2 uat value
      hosts:
        dc2uathost1
        dc2uathost2
        dc2uathost3
        dc2uathost4
    prod:
      vars:
        var1: dc2 prod value
        var2: dc2 prod value
        var3: dc2 prod value
      hosts:
        dc2prodhost1
        dc2prodhost2
        dc2prodhost3
        dc2prodhost4

1 Like

There’s your problem right there! There aren’t multiple dev groups. Groups don’t nest. It’s easy to imagine that they do, especially when you define them in a complex data structure — a dev under dc1 and a dev under dc2 — but that isn’t what’s happening.

What is happening is, you’re saying “Put these hosts associated with dc1 in the dev group, and put these other hosts associated with dc2 also in the one-and-only dev group.”

In other words, the “group namespace” is flat, not a hierarchy, regardless of how you define them.

Yeah, that’s what I expected given the results I saw. Is there a way to structure this / use a --limit that might achieve that I am looking for?

The solution, then, is to use group names that reflect the hierarchy that you wish they were in.

<dc>_<env>_<hostname>

Also include an all at the <dc> and <env> levels, and fully articulate the groups. So you would have

dcall
dc1
dc1_all
dc1_dev
dc1_uat
dc2
dc2_all
dc2_dev
dc2_uat

That may seem like overkill, but it gives you the flexibility to construct group names programmatically without having to worry about undefined group names.

I’m not quite following how the all groups would function here (when compared to the groups of the same name without all added).

Can you wrangle my example into shape to elaborate?

A fair question. It may be overdoing it a bit. However, In our experience, we do a lot of slicing and dicing with generated group names, and we’ve found them to be handy.

For example, if you need to do something across the uat groups in all data centers, you can say "dcall_uat" instead of "dc1_uat,dc2_uat".

In practice, we have a prefix for each of our inventories, so if this were in mw inventory, we’d have these groups:

mw_all
mw_all_dev
mw_all_uat
mw_all_prd
mw_dc1
mw_dc1_dev
mw_dc1_uat
mw_dc1_prd
mw_dc2
mw_dc2_dev
mw_dc2_uat
mw_dc2_prd

And of course these would have the obvious children relationships.

We used to start with only the group names we’d actually need, then back-fill the intermediate groups as needed for convenience. But we found that remembering which groups we he had and had not back-filled was more of a pain than just fully articulating the entire hierarchy from the outset. We probably define some group names that we never use, but we know we could use them because we’re pretty consistent in setting this up this way. We’ve done it across ~45 projects (where a “project” maps to the 2nd level in our mw_* inventory), so we know how our group names are defined regardless of which project we’re currently working on. (It one project, it goes to 5 levels!)

Pedantic Consistency for the Win!

If i could rewrite the YAML inventory today:

  • vars would only be allowed on groups and hosts at top level under groups and hosts keys
  • hosts and children keys under groups would be simple lists, which can ‘create’ the hosts, but not allow chaining definitions
  • I would change the children keyword to include

I believe that would clarify how the inventory works as the current model, while very convenient, that allows you to add to host/group definitions anywhere they appear, leads to much confusion about a hierarchy that does not really exist in the inventory. I hope the above helps give you a clearer picture on how to organize.

1 Like

I think this will do it. (I’m in a zoom at the moment and this is a pleasant distraction!)

[Edit: I had left out some *_all groups.]

---
all:
  vars:
    service1_user: "{{ lookup('env', 'SERVICE1_USER') }}"
    service1_password: "{{ lookup('env','SERVICE1_PASSWORD') }}"
    service2_password: "{{ lookup('env','SERVICE2_PASSWORD') }}"
    service3_user: "{{ lookup('env', 'SERVICE3_USER') }}"
    service3_pass: "{{ lookup('env', 'SERVICE3_PASS') }}"
dcall:
  children:
    dc1: {}
    dc2: {}
dc1:
  vars:
    var0: dc1 value
  children:
    dc1_all:
      children:
        dc1_dev: {}
        dc1_uat: {}
        dc1_prod: {}
    dc1_dev:
      vars:
        var1: DC1 dev value
        var2: DC1 dev value
        var3: DC1 dev value
      hosts:
        dc1devhost1
        dc1devhost2
        dc1devhost3
        dc1devhost4
    dc1_uat:
      vars:
        var1: DC1 uat value
        var2: DC1 uat value
        var3: DC1 uat value
      hosts:
        dc1uathost1
        dc1uathost2
        dc1uathost3
        dc1uathost4
    dc1_prod:
      vars:
        var1: DC1 prod value
        var2: DC1 prod value
        var3: DC1 prod value
      hosts:
        dc1prodhost1
        dc1prodhost2
        dc1prodhost3
        dc1prodhost4

dc2:
  vars:
    var0: dc2 value
  children:
    dc2_all:
      children:
        dc2_dev: {}
        dc2_uat: {}
        dc2_prod: {}
    dc2_dev:
      vars:
        var1: dc2 dev value
        var2: dc2 dev value
        var3: dc2 dev value
      hosts:
        dc2devhost1
        dc2devhost2
        dc2devhost3
        dc2devhost4
    dc2_uat:
      vars:
        var1: dc2 uat value
        var2: dc2 uat value
        var3: dc2 uat value
      hosts:
        dc2uathost1
        dc2uathost2
        dc2uathost3
        dc2uathost4
    dc2_prod:
      vars:
        var1: dc2 prod value
        var2: dc2 prod value
        var3: dc2 prod value
      hosts:
        dc2prodhost1
        dc2prodhost2
        dc2prodhost3
        dc2prodhost4```

Thanks. That more or less matches what I did as a “temporary” fix while researching if there was a solution that more accurately aligned with what I wanted.

If I were to break dc1/dc2 into distinct files (inventory/dc1.yml, inventory/dc2.yml) and then did --limit dc1 would that behave the way I expect, or would it still parse both anyway?

I don’t think splitting the inventory into different files changes anything. Any groups that are children of dc1 would be in play.

1 Like

parsing depends on what you pass to ansible as an inventory source:

-i inventory/dc1.yml will ONLY get that file
-i inventory/ will merge all inventory source files in that directory into a single inventory
-i inventory/dc1.yml -i inventory/dc2.yml will merge both files as a single inventory also

limit runs AFTER creating a unified inventory, it only acts on play/target selection, not definitions

2 Likes

@bcoca & @utoddl, thanks for taking the time to help clear that up. This is more or less the outcome I expected, but I was hoping I was missing something.

My path forward seems clear. I appreciate it.

I highly encourage you to use the ansible-inventory command to see what you’ve got with various inventory specifications. For example, I’m doing this to check my copy of your inventory:

$ ansible-inventory --graph -i Lyle_McKarns-inventory.yml
@all:
  |--@ungrouped:
  |--@dcall:
  |  |--@dc1:
  |  |  |--@dc1_all:
  |  |  |  |--@dc1_dev:
  |  |  |  |  |--dc1devhost1 dc1devhost2 dc1devhost3 dc1devhost4
  |  |  |  |--@dc1_uat:
  |  |  |  |  |--dc1uathost1 dc1uathost2 dc1uathost3 dc1uathost4
  |  |  |  |--@dc1_prod:
  |  |  |  |  |--dc1prodhost1 dc1prodhost2 dc1prodhost3 dc1prodhost4
  |  |  |--@dc1_dev:
  |  |  |  |--dc1devhost1 dc1devhost2 dc1devhost3 dc1devhost4
  |  |  |--@dc1_uat:
  |  |  |  |--dc1uathost1 dc1uathost2 dc1uathost3 dc1uathost4
  |  |  |--@dc1_prod:
  |  |  |  |--dc1prodhost1 dc1prodhost2 dc1prodhost3 dc1prodhost4
  |  |--@dc2:
  |  |  |--@dc2_all:
  |  |  |  |--@dc2_dev:
  |  |  |  |  |--dc2devhost1 dc2devhost2 dc2devhost3 dc2devhost4
  |  |  |  |--@dc2_uat:
  |  |  |  |  |--dc2uathost1 dc2uathost2 dc2uathost3 dc2uathost4
  |  |  |  |--@dc2_prod:
  |  |  |  |  |--dc2prodhost1 dc2prodhost2 dc2prodhost3 dc2prodhost4
  |  |  |--@dc2_dev:
  |  |  |  |--dc2devhost1 dc2devhost2 dc2devhost3 dc2devhost4
  |  |  |--@dc2_uat:
  |  |  |  |--dc2uathost1 dc2uathost2 dc2uathost3 dc2uathost4
  |  |  |--@dc2_prod:
  |  |  |  |--dc2prodhost1 dc2prodhost2 dc2prodhost3 dc2prodhost4

Surprises (a.k.a. mistakes) tend to jump out.

1 Like

Yeah, that’s what I did to confirm what was happening with the groups merging. I didn’t think to do it before hand because I thought I knew what I was doing (I clearly did not). It very quickly showed me what was happening.

1 Like

I was using it before posting, and managed to completely ignore the purple error messages. :slight_smile:
I’ve edited (again) the revised inventory I posted above.

If you end up taking a different approach, I’d like to see what you did.

Good luck!