Покрываем плэйбуками ansible IaC в части предоставления доступа

935565e6cdce4ebfe3a99445c6b790e2.jpg

Инфраструктура как код (Infrastructure-as-Code; Iac) — это подход для управления и описания инфраструктуры ЦОД через конфигурационные файлы, а не через ручное редактирование конфигураций на серверах или интерактивное взаимодействие.

Итак мы уже научились быстро поднимать инфраструктуру с помощью IaC, завели кучу репозиторриев и готовы восстановить исковерканную или сломанную инфраструктуру в части готовности сервисов. В качесте следующего этапа можно рассмотреть предоставление доступов к ресурсам и документирование предоставления таких доступов. Такой подход позволит не только быстро находить на каком основании Вася дропнул табличку на проде, но и ускорить предоставление доступов и сократить количество ошибок.

Я покажу своё видение структуры такой ansible-роли (далее — роль).

Начнем с tasks/main.yml:

---
# Проверка корректности конфигурационных файлов описанных в IaC
# Проверки корректности и имен и взаимосвязей 
- name: Check
  include_tasks:
    file: check.yml
  tags: always
  run_once: true

# Установка на целевой хост пакетов необходимых для работы 
# ansible-модулей 

- name: installing the required packages
  apt:
    name: "{{ item }}"
  loop:
    - required_package_1
    - required_package_2

# Получение имени последнего коммита.
# Его можно использовать в качестве темы письма, а также
# проводить различные проверки на его содержание.

- name: get latest commit message
  shell: git log --pretty="%s" -n1
  register: latest_commit_message
  tags: always
  run_once: true
  delegate_to: localhost
  changed_when: false

# Сбор фактов о существующих сущностях на которые выдаются права 
# и правах предоставленных пользователям

- name: Collect info about entities, roles
  become: true
  become_user: postgres
  community.postgresql.postgresql_info:
    login_unix_socket: '{{ pg_login_unix_socket }}'
    db: postgres
    filter:
      - "databases"
      - "roles"
  register: "pg_exist"
# Тегирование -  важный элемент формирования роли. 
# Позволит нам изорировать работу с различным видом сущностей.
  tags:
    - pg_db_config
    - pg_roles_config
  run_once: true

# Последовательная работа с ролями

- name: Work with roles
  include_tasks:
    file: pg_roles.yml
    apply:
      tags: pg_roles_config
  when: pg_roles_config
  tags: pg_roles_config
  run_once: true

# работа с сущностями

- name: Work with db
  include_tasks:
    file: pg_db.yml
    apply:
      tags: pg_db_config
  when: pg_db_config
  tags: pg_db_config
  run_once: true

# работа с дополнительными механизмами, обеспечмвающими доступ 

- name: Work with pg_hba
  include_tasks:
    file: pg_hba.yml
    apply:
      tags: pg_hba_config
  when: pg_hba_config
  tags: pg_hba_config
  # run_once: true

# Вывод ошибок полученных в рамках работ, естественно красным цветом

- name: Fail message
  debug:
    msg: "Не все конфиги были применены \n{{ rescue_msgs|to_nice_yaml }}"
  failed_when: true
  when: rescue_msgs != []
  tags: always
  run_once: true

Здесь приведен пример сбора фактов с использованием готового модуля (строка 35), однако если такового нет придется поработать шелом и жинжа фильтрами. После выполнения команды с модулем command/shell можно сохранить вывод в переменной с помощью параметра register. Это позволит использовать вывод в дальнейших задачах плейбука. Ansible позволяет использовать фильтры Jinja2 для обработки сохраненного вывода. Фильтры позволяют разбирать строки, извлекать подстроки, разделить текст на отдельные элементы и многое другое.

- name: Run a shell command and parse the output using regex
  hosts: your_target_hosts
  tasks:
    - name: Execute the shell command
      command: your_shell_command
      register: shell_output

    - name: Process the output using regex
      debug:
        msg: "{{ shell_output.stdout | regex_search('pattern') }}"

При сборе фактов формируем словари-списки идентичные тем, которые описаны в IaC. То есть для последующего сравнения мы должны получить пары:

pg_db_exist # существующие сущности
pg_db_iac # сущности, описанные в IaC

На примере работы с базами данных рассмотрим порядок работы с сущностями.

---
# Формирование идентичного словаря
- name: Parse db from output
  set_fact:
    pg_db_exist: |
      {{pg_db_exist|combine({item.key: {'access_priv': item.value.access_priv|regex_replace("/.*\n","/")|split("/")|map('regex_search', '.*=.*')|select('string')|list, 'owner': item.value.owner}})}}
  loop: "{{ pg_exist.databases|dict2items }}"
  when: pg_exist.databases != {}

# Формирование словаря на сущностей для создания
- set_fact:
    db_dict_to_create: '{{ db_dict_to_create | combine({ item.key : item.value })}}'
  loop: "{{ pg_db_iac|dict2items }}"
  when: item.key not in pg_db_exist

# Формирование словаря на сущностей для изменения
- set_fact:
    db_dict_to_alter: '{{ db_dict_to_alter | combine({ item.key : item.value })}}'
  loop: "{{ pg_db_iac|dict2items }}"
  when: item.key in pg_db_exist and item.value != pg_db_exist[item.key]

# Формирование словаря на сущностей для удаления
- set_fact:
    db_dict_to_delete: '{{ db_dict_to_delete | combine({ item.key : item.value })}}'
  loop: "{{ pg_db_exist|dict2items }}"
  when: item.key not in pg_db_iac

# Защита от автоматического удаления сущностей на проде
- name: Verify required db
  assert:
    that: db_dict_to_delete == {}
    fail_msg: "You need to delete databases. Set db_auto_delete=true"
  when: not db_auto_delete
# Вывод информации о том, что было, что будет, чем сердце успокоится
# он поможет нам не только отлаживаться в рамках рутинной работы, 
# но и сформировать первичный словарь/список, который будет внесен в IaC
- debug:
    msg: "pg_db_exist\n {{ pg_db_exist|to_yaml }}\n\n pg_db_iac\n {{ pg_db_iac|to_yaml }}\n\n/
  {%- if db_dict_to_alter != {} -%}DB will be altered\n{{ db_dict_to_alter|to_yaml }}\n\n{% endif %}
  {%- if db_dict_to_create != {} -%}DB will be created\n{{ db_dict_to_create|to_yaml }}\n\n{% endif %}
  {%- if db_dict_to_delete != {} -%}DB will be deleted\n{{ db_dict_to_delete|to_yaml }}\n\n{% endif %}"

# Сами действия с сущностями
- name: Delete databases
  include: pg_db_action.yml
  vars:
    pg_db_action: 'absent'
  loop: "{{ db_dict_to_delete|dict2items }}"
  when: db_auto_delete and db_dict_to_delete != {}

- name: Create databases
  include: pg_db_action.yml
  vars:
    pg_db_action: 'present'
    pg_db_create: true
    loop_access_priv: "{{ item.value.access_priv }}"
  loop: "{{ db_dict_to_create|dict2items }}"
  when: db_dict_to_create != {}

- name: Update databases
  include: pg_db_action.yml
  vars:
    pg_db_action: 'present'
    pg_db_update: "{{ pg_db_exist }}"
    access_priv_out: "{{ pg_db_exist[item.key].access_priv | difference(item.value.access_priv) }}"
    access_priv_in: "{{ item.value.access_priv | difference(pg_db_exist[item.key].access_priv) }}"
  loop: "{{ db_dict_to_alter|dict2items }}"
  when: db_dict_to_alter != {}

Формирование отладочной информации — важный этап, который поможет понять почему не отработали скрипты и где закралась ошибка. Понятное, дело чем больше кода мы покроем такой обработкой — тем больше отладочной информации получим. Рассмотрим пример.

# Работаем с блоками ansible
- name: Block
  block:
    - name: Action {{ pg_db_action }} a database with name {{ item.key }}
      become: true
      become_user: postgres
      community.postgresql.postgresql_db:
        login_unix_socket: '{{ pg_login_unix_socket }}'
        name: "{{ item.key }}"
        state: "{{ pg_db_action }}"
        owner: "{{ item.value.owner }}"
      register: actionresult
# В случае неудачи мы не падаем  и выходим из роли, 
# а продолжаем выполнение, записав ошибку в список ошибок,
# который мы выводим в конце роли 
  rescue:
    - set_fact:
        rescue_msgs: "{{ rescue_msgs|default([]) + 
        [ 'Action '+pg_db_action+' a database with name ' 
        + item.key+' unsucsessful because '+actionresult.msg] }}"

Надеюсь, что данная статья поможет структурировать мысли и оформить вашу инфраструкту.

© Habrahabr.ru