Инфраструктура как код (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] }}"
Надеюсь, что данная статья поможет структурировать мысли и оформить вашу инфраструкту.
