Ansible: теги, include_vars и приоритеты переменных

By | 05/22/2018
 

В процессе написания плейбука для создания стека с модулем cloudformation (про него позже) появился пример использования тегов для ограничения выполнения задач из различных ролей, и хороший пример важности учёта приоритетов при использовании переменных.

Ansible теги

К примеру, есть такой плейбук:

- hosts:
  - all
  become:
    true
  gather_facts:
    false
  roles:
    - role: cloudformation
      tags: infra
    - role: common
      tags: app

Зачем запускать CloudFormation стек каждый раз при вызове плейбука?

Что бы ограничить роли, которые будут запускаться – добавляем теги, тут это tags: infra и tags: app.

А далее, ри вызове ansible-playbook, выбираем что именно хотим запустить – роль для инфрастуктуры (tags: infra):

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-production --tags infra mobilebackend.yml
PLAY [all] ****
TASK [cloudformation : Echo] ****
ok: [mb.btrm.com] => {
"msg": "Running CloudFormation role"
}

Или роль для настройки самого сервера приложения (tags: app):

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-production --tags app mobilebackend.yml
PLAY [all] ****
TASK [common : Echo] ****
ok: [mb.btrm.com] => {
"msg": "Running Common role"
}

Учитывая, что ролей для настройки сервера будет намного больше, чем одна – то можно уменьшить необходимость в постоянном повторении тега app для каждой роли, и оставить тег только для роли CloudFormation, например так:

- hosts:
  - all
  become:
    true
  gather_facts:
    false
  roles:
    - role: cloudformation
      tags: infra
    - role: common

Далее можно использовать ansible-playbook и передавать --skip-tags infra, что бы игнорировать CloudFomation, и запускать все остальные роли:

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-production --skip-tags infra mobilebackend.yml
PLAY [all] ****
TASK [common : Echo] ****
ok: [mb.btrm.com] => {
"msg": "Running Common role"
}

ansible_connection, теги и порядок приоритетов переменных

См. Non-SSH connection types.

Ещё один момент – это хост для запуска команд Ansible с указанием типа подключения.

Роль cloudformation необходимо вызывать локально, а роли common и другие – на удалённом хосте.

И тут мы сталкиваемся с одной из наиболее важных особенностей Ansible – порядок приоритетов переменных.

Рассмотрим на более наглядном примере плейбука:

- hosts:
  - all
  become:
    true
  gather_facts:
    false
  vars:
    rnum: playbook
  roles:
    - role: role1
      tags: tag1
    - role: role2
      tags: tag2

Файл инвентори – hosts:

[all]
host1

[all:vars]
rnum="hosts"

ansible.cfg:

[defaults]
rnum = "ansible.cfg"

Все они содержат переменную rnum, каждая со своим значением.

Файл main.yml из tasks обеих ролей одинаковые:

- name: Echo
  debug: msg="role number {{ rnum }}"

А main.yml в vars – разные:

cat roles/role1/vars/main.yml
rnum: one

И роль2:

cat roles/role2/vars/main.yml
rnum: two

При выполнении ролей сейчас результат ожидаем:

ansible-playbook -i hosts playbook.yml
PLAY [all] ****
TASK [role1 : Echo] ****
ok: [host1] => {
"msg": "role number one"
}
TASK [role2 : Echo] ****
ok: [host1] => {
"msg": "role number two"
}

А теперь – удалим значение rnum из roles/role1/vars/main.yml, и вызовем плейбук, но без роли role2.

По идее – Ansible “скипнет” переменную из роли2, раз мы её не вызываем, и подставит rnum из файла плейбука:

...
  vars:
    rnum: playbook
...

Пробуем:

ansible-playbook -i hosts playbook.yml --skip-tags tag2
PLAY [all] ****
TASK [role1 : Echo] ****
ok: [host1] => {
"msg": "role number two"
}

Упс…

Почему two?

Читаем описание tags:

Adding “tags:” in any part of a play (including roles) adds those tags to the contained tasks.

Т.е. получается, что --skip-tags применяется только к таскам, а roles/role2/vars/main.yml считывается в любом случае.

А так как в приоритетах переменные самой роли имеют больший вес, что ansible.cfg, hosts и vars из плейбука – то мы получаем значение rnum: two из файла vars/main.yml второй роли.

Почему это важно сейчас?

Вернёмся к началу:

Роль cloudformation нам необходимо вызывать локально, а роли common и другие – на удалённом хосте.

Указание на среду выполнения мы передаём через значение для ansible_connection, и если для ansible_connection указать значение local в файле roles/common/vars/main.yml – то этот самый local будет применяться ко всем ролям и таскам, независимо от --skip-tags.

Т.е. – модуль ping из roles/common/tasks/main.yml будет выполняться независимо от того – есть подключение к хосту из hosts, или нет. А нам этого не надо:

- name: Echo
  debug: msg="Running Common role"

- name: Ping
  ping:

Тут варианта два: либо указывать ansible_connection=ssh в vars укаждой роли, а этого не хочется, ибо зачем нам повторять код?, либо – подключать файл с переменными из задачи в роли cloudformation. Тогда задача считывания переменных будет “скипаться” при --skip-tags infra, переменная ansible_connection не будет задаваться в local, и “проблема” решена.

Решаем это  спомощью include_vars.

Обновляем roles/cloudformation/tasks/main.yml:

- name: Read CloudFormation vars
  include_vars: cf_vars.yml

- name: Echo
  debug: msg="Running CloudFormation role"

Создаём roles/cloudformation/vars/cf_vars.yml:

...
ansible_connection: local
...

Запускаем:

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-dev --skip-tags app mobilebackend.yml
PLAY [all] ****
TASK [cloudformation : Read CloudFormation vars] ****
ok: [mbdev.btrm.com]
TASK [cloudformation : Echo] ****
ok: [mbdev.btrm.com] => {
"msg": "Running CloudFormation role"
}

А вот роль common будет фейлится, т.к. в ней есть вызов ping, при вызове которого Ansible попробует подключиться к хосту из hosts, но домен там пока “фейковый”, и пинг не сработает:

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-dev --skip-tags infra mobilebackend.yml
PLAY [all] ****
TASK [common : Echo] ****
ok: [mbdev.btrm.com] => {
"msg": "Running Common role"
}
TASK [common : Ping] ****
fatal: [mbdev.btrm.com]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname mbdev.btrm.com: Name or service not known\r\n", "unreachable": true}

Если бы common вызывала ping через ansible_connection: local – то ping сработал бы.

include_vars и отдельные файлы переменных для окружений

Что ещё хотелось  бы сделать перед тем как создавать стек – это иметь возможность переопределить переменные в зависимости от значения переменной env.

Добавляем env в hosts:

[btrm-mobilebackend-dev]
mbdev.btrm.com

[btrm-mobilebackend-dev:vars]
env=dev

[btrm-mobilebackend-production]
mb.btrm.com

[btrm-mobilebackend-production:vars]
env=production

Далее, в roles/cloudformation/tasks/main.yml используем переменную env, что бы выбрать файл с переменными:

- name: Read ENV specific variables
  include_vars:
    file: "{{ env }}_vars.yml"

- name: Read CloudFormation global vars
  include_vars: cf_vars.yml

- name: Echo
  debug: msg="Running CloudFormation role on {{ envname }}"

Создаём два файла с переменными – dev_vars.yml:

cat roles/cloudformation/vars/dev_vars.yml
envname: "Dev stack"

И production:

cat roles/cloudformation/vars/production_vars.yml
envname: "Production stack"

Запускаем cloudformation роль на Dev окружении (--limit btrm-mobilebackend-dev):

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-dev --skip-tags app mobilebackend.yml
PLAY [all] ****
TASK [cloudformation : Read ENV specific variables] ****
ok: [mbdev.btrm.com]
TASK [cloudformation : Read CloudFormation global vars] ****
ok: [mbdev.btrm.com]
TASK [cloudformation : Echo] ****
ok: [mbdev.btrm.com] => {
"msg": "Running CloudFormation role on Dev stack"
}

И на Проде:

ansible-playbook -i hosts.ini --limit btrm-mobilebackend-production --skip-tags app mobilebackend.yml
PLAY [all] ****
TASK [cloudformation : Read ENV specific variables] ****
ok: [mb.btrm.com]
TASK [cloudformation : Read CloudFormation global vars] ****
ok: [mb.btrm.com]
TASK [cloudformation : Echo] ****
ok: [mb.btrm.com] => {
"msg": "Running CloudFormation role on Production stack"
}

Готово.