Tying to understand ansible variables and vault with sub-plays

Viewed 408

I'm having a very difficult time understanding how to organize large playbooks with many roles, using inventory with multiple "environments" and using sub-plays to try and organize things. All the while having common variables at the parent playbook, sharing those with sub-plays. I use ansible but in a very limited way so I'm trying to expand my knowledge of it by doing this exercise.

Directory structure (simplified for testing)

├── inventory
│   ├── dev
│   │   ├── group_vars
│   │   │   └── all.yml
│   │   └── hosts
│   └── prod
│       ├── group_vars
│       │   └── all.yml
│       └── hosts
├── playbooks
│   └── infra
│       └── site.yml
├── site.yml
└── vars
    └── secrets.yml

Various secrets are in the secrets.yml file, including the ansible_ssh_user and ansible_become_pass.

Contents of all.yml

---
ansible_ssh_user: "{{ vault_ansible_ssh_user }}"
ansible_become_pass: "{{ vault_ansible_become_pass }}"

Contents of site.yml

---
- name: test plays
  hosts: all
  vars_files:
    - vars/secrets.yml
  become: true
  gather_facts: true
  pre_tasks:
    - include_vars: secrets.yml

  tasks:
    - debug:
        var: ansible_ssh_user

- import_playbook: playbooks/infra/site.yml

Content of playbooks/infra/site.yml

---
- name: test sub-play
  hosts: all
  become: true
  gather_facts: true

  tasks:
    - debug:
        var: ansible_ssh_user

The main parent playbook is being called with ansible-playbook -i inventory/dev site.yml. The problem is I can't access vault_ansible_ssh_user or vault_ansible_become_pass (or any secrets in vault) from within the sub-play if I don't include both var_files AND pre_tasks: - include_vars

If I remove var_files, I can't access the secrets in the parent playbook. If I remove pre_tasks: - include_vars, I can't access any secrets in the imported sub-play. Any idea why I need both of these variable include statements for this to work? Also, is this just a terrible design and I'm doing things completely wrong? I'm having a hard time wrapping my head around the best way to organize huge playbooks with a lot of required variables so I ended up with a directory structure like this to try and compartmentalize the variables to avoid very large variables files and the need to duplicate variable files all over the place. This probably just boils down to me wanting to fit a round peg in a square hole but I can't find a great best practices example for something like this.

This issue might also have to do with me trying to put ansible vault variables in an inventory var file maybe. If so, is that something I should or shouldn't be doing? As I was writing this, I may have had a "light bulb" moment and finally understand how I should handle this but I need to test some things to understand it fully but regardless, I'm still very interested in what the stackoverflow community has to say about how I'm currently doing this.

EDIT: turns out my "light bulb" idea is just the same as I have here just moved around in a different way, with the same issues

1 Answers

Q: "If I remove ... include_vars, I can't access any secrets in the imported sub-play."

A: To share variables among the plays use include_vars or set_fact. Quoting from Variable scope: how long is a value available?

Variable values associated directly with a host or group, including variables defined in inventory, by vars plugins, or using modules like set_fact and include_vars, are available to all plays. These ‘host scope’ variables are also available via the hostvars[] dictionary.

Given the files below

shell> cat inventory/prod/hosts
test_01
test_02

shell> cat inventory/prod/group_vars/all.yml
ansible_ssh_user: "{{ vault_ansible_ssh_user }}"
ansible_become_pass: "{{ vault_ansible_become_pass }}"

shell> cat vars/secrets.yml 
vault_ansible_ssh_user: ansible-ssh-user
vault_ansible_become_pass: ansible-become-pass

shell> cat site.yml 
- name: test plays
  hosts: all
  gather_facts: false
  vars_files: vars/secrets.yml
  tasks:
    - debug:
        var: ansible_ssh_user
    - debug:
        var: ansible_become_pass

- import_playbook: playbooks/infra/site.yml

shell> cat playbooks/infra/site.yml
- name: test sub-plays
  hosts: all
  gather_facts: false
  tasks:
    - debug:
        var: ansible_ssh_user

The variables declared by vars_files are not shared among the plays and the second play will fail. The abridged result is

shell> ANSIBLE_INVENTORY=$PWD/inventory/prod/hosts ansible-playbook site.yml

PLAY [test plays] ****

TASK [debug] ****
ok: [test_01] => {
    "ansible_ssh_user": "ansible-ssh-user"
}
ok: [test_02] => {
    "ansible_ssh_user": "ansible-ssh-user"
}

TASK [debug] ****
ok: [test_01] => {
    "ansible_become_pass": "ansible-become-pass"
}
ok: [test_02] => {
    "ansible_become_pass": "ansible-become-pass"
}

PLAY [test sub-plays] ****

TASK [debug] ****
fatal: [test_01]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable. The error was: 'vault_ansible_become_pass' is undefined"}
fatal: [test_02]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable. The error was: 'vault_ansible_become_pass' is undefined"}

The problem will disappear if you use include_vars or set_fact, i.e. "instantiate" the variables. Commenting set_fact and uncommenting include_vars, or uncommenting both, will give the same result

- name: test plays
  hosts: all
  gather_facts: false
  vars_files: vars/secrets.yml
  tasks:
    - debug:
        var: ansible_ssh_user
    - debug:
        var: ansible_become_pass
#   - include_vars: secrets.yml
    - set_fact:
        ansible_ssh_user: "{{ ansible_ssh_user }}"
        ansible_become_pass: "{{ ansible_become_pass }}"

- import_playbook: playbooks/infra/site.yml

Then the abridged result is

shell> ANSIBLE_INVENTORY=$PWD/inventory/prod/hosts ansible-playbook site.yml

PLAY [test plays] ****

TASK [debug] ****
ok: [test_01] => {
    "ansible_ssh_user": "ansible-ssh-user"
}
ok: [test_02] => {
    "ansible_ssh_user": "ansible-ssh-user"
}

TASK [debug] ****
ok: [test_01] => {
    "ansible_become_pass": "ansible-become-pass"
}
ok: [test_02] => {
    "ansible_become_pass": "ansible-become-pass"
}

TASK [set_fact] ****
ok: [test_01]
ok: [test_02]

PLAY [test sub-plays] ****

TASK [debug] ****
ok: [test_02] => {
    "ansible_ssh_user": "ansible-ssh-user"
}
ok: [test_01] => {
    "ansible_ssh_user": "ansible-ssh-user"
}

Notes

  • In this example, it's not important whether the variables are encrypted or not.

  • become and gather_facts don't influence this problem.

  • There might be other issues. It's a good idea to review include and import issues.


Q: "Why is the vars_files needed?"

A: The variable ansible_become_pass is needed to escalate the user's privilege when a task is sent to the remote host. As a result, when the variable vault_ansible_become_pass is declared in the task include_vars only, this variable won't be available before the tasks are executed, and the play will fail with the error

fatal: [test_01]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable. The error was: 'vault_ansible_become_pass' is undefined"}

See


No vars_files is needed if there are user-defined variables only. For example, the playbook below works as expected

shell> cat inventory/prod/group_vars/all.yml
var1: "{{ vault_var1 }}"
var2: "{{ vault_var2 }}"

shell> cat vars/secrets2.yml 
vault_var1: test-var1
vault_var2: test-var2

shell> cat site2.yml
- name: test plays
  hosts: all
  gather_facts: false
  tasks:
    - include_vars: secrets2.yml
    - debug:
        var: var1
    - debug:
        var: var2

- import_playbook: playbooks/infra/site2.yml

shell> cat playbooks/infra/site2.yml
- name: test sub-plays
  hosts: all
  gather_facts: false
  tasks:
    - debug:
        var: var1
    - debug:
        var: var2
Related