English 中文(简体)
In ansible, what s the best way to gather and merge vars from different roles and use in a dependent role?
原标题:
  • 时间:2023-06-22 14:33:53
  •  标签:
  • ansible

If I have a common baseline Ansible role but want to contribute data to that role from other different modules, what s the best way to achieve this?

For example: I have a role called firewall that manages a system s firewall, and two other roles webserver and mailserver. The webserver role knows it needs to configure a firewall rule allowing traffic on ports tcp80/443, and mailserver knows it needs to contribute a firewall rule allowing traffic on tcp25.

I only want to execute the firewall role once.

If I add firewall as dependencies of mailserver and webserver and called with different vars, the firewall rule runs multiple times.

One option might be to have firewall run last of all, and have the other roles run set_fact, appending their own list of rules to an existing variable that firewall can then apply.

Is there a better way?

问题回答

The tasks of a dependant role are running before the role in which it is listed as a dependency, so, this does not really seems like it fits your use case.

This said, your use case looks like the perfect fit for a handler. The said handler can be made available as part of a role.

Given:

roles/firewall/handlers/main.yml:

- name: List ports
  debug:
    msg: "Configure firewall here with {{ item }}"
  loop: "{{ firewall_ports | default([]) }}"
  listen: "firewall : Configure firewall"

- name: Second task
  debug:
    msg: This is a example of a second task needed to configure a firewall
  listen: "firewall : Configure firewall"

roles/mailserver/meta/main.yml:

dependencies:
  - role: firewall

roles/mailserver/tasks/main.yml:

- set_fact:
    firewall_ports: "{{ firewall_ports | default([]) + [25] }}"
  # Note that we do need a changed state to notify a handler
  # hence we need a `changed_when: true` 
  # since this is the only task of the role
  # Since you will have other tasks, make sure to place the `notify`
  # on a task causing a changed state
  changed_when: true
  notify: "firewall : Configure firewall"

roles/webserver/meta/main.yml:

dependencies:
  - role: firewall

roles/webserver/tasks/main.yml:

- set_fact:
    firewall_ports: "{{ firewall_ports | default([]) + [80, 443] }}"
  # Note that we do need a changed state to notify a handler
  # hence we need a `changed_when: true` 
  # since this is the only task of the role
  # Since you will have other tasks, make sure to place the `notify`
  # on a task causing a changed state
  changed_when: true
  notify: "firewall : Configure firewall"

Playbook:

- hosts: localhost
  gather_facts: false

  roles:
    - webserver
    - mailserver

Would yield:

PLAY [localhost] *********************************************************

TASK [webserver : set_fact] **********************************************
changed: [localhost]

TASK [mailserver : set_fact] *********************************************
changed: [localhost]

RUNNING HANDLER [firewall : List ports] **********************************
ok: [localhost] => (item=80) => 
  msg: configure firewall here with 80
ok: [localhost] => (item=443) => 
  msg: configure firewall here with 443
ok: [localhost] => (item=25) => 
  msg: configure firewall here with 25

RUNNING HANDLER [firewall : Second task] *********************************
ok: [localhost] => 
  msg: This is a second task needed to configure a firewall

You can merge such lists on your own if you control the roles and the namespace. For example, create defaults

shell> cat roles/mailserver/defaults/main/fw.yml 
ms_fw_allow: [25]
shell> cat roles/webserver/defaults/main/fw.yml 
ws_fw_allow: [80, 443]
shell> cat roles/firewall/defaults/main/fw.yml 
fw_fw_allow: []

In a play, include the roles and set public: true

"This option dictates whether the role’s vars and defaults are exposed to the play. If set to true the variables will be available to tasks following the include_role task."

    - include_role:
        name: "{{ item_role }}"
        public: true
      loop:
        - webserver
        - mailserver
        - firewall
      loop_control:
        loop_var: item_role

In the role firewall find all variables that match the pattern .*_fw_allow

- debug:
    msg: "{{ lookup( varnames ,  .*_fw_allow ) }}"

gives

  msg: ws_fw_allow,ms_fw_allow,fw_fw_allow

and merge(flatten) the lists

- set_fact:
    fw_allow: "{{ lookup( vars , *q( varnames ,  .*_fw_allow ))|
                  flatten|unique }}"

gives

  fw_allow: [80, 443, 25]

Depending on your use case you can put the declaration of fw_allow elsewhere. For example, into the role s defaults

shell> cat roles/firewall/defaults/main/fw.yml
fw_fw_allow: []
fw_allow: "{{ lookup( vars , *q( varnames ,  .*_fw_allow ))|
              flatten|unique }}"

Make sure the name of the variable fw_allow doesn t conflict with the pattern .*_fw_allow . Use this list to configure the firewall.


Example of a complete project for testing

shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
    ├── firewall
    │   ├── defaults
    │   │   └── main
    │   │       └── fw.yml
    │   └── tasks
    │       └── main.yml
    ├── mailserver
    │   ├── defaults
    │   │   └── main
    │   │       └── fw.yml
    │   └── tasks
    │       └── main.yml
    └── webserver
        ├── defaults
        │   └── main
        │       └── fw.yml
        └── tasks
            └── main.yml
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
localhost
shell> cat roles/webserver/defaults/main/fw.yml 
ws_fw_allow: [80, 443]

shell> cat roles/mailserver/defaults/main/fw.yml 
ms_fw_allow: [25]

shell> cat roles/firewall/defaults/main/fw.yml 
fw_fw_allow: []
fw_allow: "{{ lookup( vars , *q( varnames ,  .*_fw_allow ))|
              flatten|unique }}"
shell> cat roles/webserver/tasks/main.yml 
- debug:
    msg: Install webserver.

shell> cat roles/mailserver/tasks/main.yml 
- debug:
    msg: Install mailserver.

shell> cat roles/firewall/tasks/main.yml 
- debug:
    msg: "{{ lookup( varnames ,  .*_fw_allow ) }}"
  when: debug|d(false)|bool

- debug:
    var: fw_allow|to_yaml
  when: debug|d(false)|bool

Add other lists if you want to. For example, local_fw_allow

shell> cat pb.yml 
- hosts: all

  vars:

    local_fw_allow: [22]

  tasks:

    - include_role:
        name: "{{ item_role }}"
        public: true
      loop:
        - webserver
        - mailserver
        - firewall
      loop_control:
        loop_var: item_role

gives

shell> ansible-playbook -e debug=true pb.yml 

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

TASK [include_role : {{ item_role }}] **************************************************************

TASK [webserver : debug] **********************************************************************
ok: [localhosts] => 
  msg: Install webserver.

TASK [mailserver : debug] *********************************************************************
ok: [localhosts] => 
  msg: Install mailserver.

TASK [firewall : debug] ***********************************************************************
ok: [localhosts] => 
  msg: ws_fw_allow,ms_fw_allow,fw_fw_allow,local_fw_allow

TASK [firewall : debug] ***********************************************************************
ok: [localhosts] => 
  fw_allow|to_yaml: |-
    [80, 443, 25, 22]

PLAY RECAP ************************************************************************************
localhosts: ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0




相关问题
Related Docker networks for apps in separate Ansible roles

Suppose I m using a webserver / reverse proxy (traefik, nginx, apache, etc.) for various apps. If the webserver and all the apps are in the same docker-compose.yml then it s easy to configure the ...

Lookup method interpreting result

I am using a lookup method which returns a password (using cyberark.. but that is not relevant for this issue description) When the password happens to contain a curly brace and there is a variable in ...

利用胶卷更新多个组的库存档案

我正在尝试在多个位置更新特定剧本的库存文件。这是被简化的剧本。请注意,实际环境中已删除引用的配置。

How to fallback to a default value when ansible lookup fails?

I was a little bit surprised to discover that his piece of code fails with an IOError exception instead of defaulting to omitting the value. #!/usr/bin/env ansible-playbook -i localhost, --- - hosts: ...

热门标签