Всем привет! Сегодня я хочу поделиться с вами простым паттерном использования ansible к которому я пришёл за годы работы с этим инструментом. Несмотря на простоту, в типовой инфраструктуре он покрывает процентов 80 кейсов IaC, т.е. является довольно универсальным.
Раз за разом описывая инфраструктуру в своих плейбуках, я заметил, что в большинстве случаев повторяется следующая последовательность:
-
Мы устанавливаем на сервер какие-то пакеты.
-
Копируем на сервер необходимые файлы (конфиги, скрипты, сертификаты etc.).
-
Запускаем нужные сервисы и добавляем их в автозагрузку.
Плэйбуки распухают в основном на п.2, т.к. общепринято писать по таску template/copy
на каждый файл. В какой-то момент я понял, что любое количество файлов можно доставить на серверы всего двумя тасками или тремя, если требуются специфичные права доступа. Собственно так и родился паттерн, о котором идёт речь. Он прошёл проверку временем и сейчас я использую его повсеместно. Он идеально подходит в случае если инфраструктуру нужно покрыть кодом максимально быстро. Конечно же за любое упрощение приходится чем-то платить и паттерн имеет свои недостатки и ограничения, но об этом ниже.
Начну пожалуй со структуры репозитория.
├── playbooks/ │ ├── files/ │ └── site.yml └── site.yml
-
site.yml в корне проекта является своеобразной точкой входа, где при помощи
import_playbook
мы описываем верхнеуровневую структуру проекта, какие плэйбуки из каталога playbooks/ подключаются, в какой последовательности, тут же удобно навесить нужные теги и т.п. -
Все файлы, которые требуется выложить на серверы, размещаем в
playbooks/files/
с теми же путями, по которым эти файлы должны располагаться на серверах. Например, если на сервер надо выложить сертификат /etc/ssl/certs/nginx-selfsigned.crt, то в репозитории мы помещаем его в playbooks/files/etc/pki/tls/certs/nginx-selfsigned.crt.
Далее приведу пример плэйбука с комментариями.
--- - name: Setup nginx hosts: all # Параметр для того чтобы ansible-playbook в check mode не завершался после первой ошибки. # Так мы можем собрать больше ошибок за каждый прогон и сэкономить время. ignore_errors: '{{ ansible_check_mode }}' # Необходимые переменные, описываю здесь для наглядности. vars: rpm_packages: - nginx reload_services: - nginx restart_services: [] tasks: - name: Install packages ansible.builtin.dnf: name: '{{ rpm_packages }}' state: present update_cache: true when: rpm_packages is defined and rpm_packages # Таск пробежит по дереву каталогов в files/ и создаст недостающие на серверах. # noqa - подсказка для ansible-lint о том, что права доступа опущены целенаправленно. - name: Create directories tree # noqa: risky-file-permissions ansible.builtin.file: path: '/{{ item.path }}' state: directory with_community.general.filetree: files/ when: item.state == 'directory' # Таск пробежит по дереву каталогов в files/ и скопирует на серверы все файлы в этих каталогах. # Т.к. мы используем ansible.builtin.template, то файлы расцениваются как шаблоны jinja2. - name: Copy config files # noqa: risky-file-permissions ansible.builtin.template: src: '{{ item.src }}' dest: '/{{ item.path }}' with_community.general.filetree: files/ when: item.state == 'file' notify: - Reload services - Restart services # Таск нужен для финальной корректировки прав доступа к файлам и каталогам, # т.к. при "пакетном" создании тасками выше установить специфичные права доступа не получится. - name: Set files permissions ansible.builtin.file: path: '{{ item.path }}' owner: '{{ item.owner | d("root") }}' group: '{{ item.group | d("root") }}' mode: '{{ item.mode | d("0600") }}' loop: - path: /etc/pki/tls/certs/nginx-selfsigned.crt - path: /etc/pki/tls/private/nginx-selfsigned.key - path: /usr/local/bin/example_script mode: '0755' - name: Start services ansible.builtin.systemd: name: '{{ item }}' state: started enabled: true daemon_reload: true loop: '{{ reload_services + restart_services }}' handlers: - name: Reload services ansible.builtin.systemd: name: '{{ item }}' state: reloaded loop: '{{ reload_services }}' when: reload_services is defined and reload_services - name: Restart services ansible.builtin.systemd: name: '{{ item }}' state: restarted loop: '{{ restart_services }}' when: restart_services is defined and restart_services
И это всё. Пять тасков + два хендлера = 80% IaC для bare metal/VM. Дополнительным преимуществом паттерна является структура каталогов в files/
аналогичная тому как файлы/каталоги располагаются на серверах. Обычно если в templates
/ просто свалены шаблоны, то далеко не всегда из названия файла можно понять к какому сервису он относится. Надо открыть файл и/или заглянуть в плэйбук. Это лишние действия. Но если дерево каталогов в репозитарии такое же, как на серверах, то мы сразу же можем понять что к чему относится и можем просто добавлять новые каталоги/файлы не заглядывая в плэйбук. Процесс создания IaC становится максимально простым и быстрым: копипастим с минимальными правками плейбук из пяти тасков, добавляем в переменные три списка (rpm_packages
, reload_services
, restart_services
), а дальше можем полностью сконцентрироваться на самой конфигурацуии в files/
.
Теперь стоит поговорить о недостатках и подхода и способах их устранения или смягчения.
-
Т.к. все файлы мы копируем на серверы одним таском, то нет простого способа перезапустить только сервисы в конфигурации которых были изменения. Это несущественно для сервисов которые умеют перечитывать конфигурацию по сигналу, но для тех немногих, которые не умеют, это может являться серьёзной проблемой. И действительно, идея перезагружать, например, боевую СУБД при любом изменении любых файлов является сомнительной, хотя приемлемой там, где это допускает SLA.
-
Второй существенный недостаток проистекает опять же из копирования файлов одним таском. Если вы используете CI, а я надеюсь, что вы используете, то в журналы пайплайнов могут попасть чувствительные данные (пароли, ключи, сертификаты etc.). Эту проблему можно решить «в лоб» добавив в таск параметр «
no_log: true
«, но тогда мы потеряем возможность видеть изменения в файлах при выполнении ansible-playbook. Вариант конечно рабочий, но мне не нравится.
К счастью, обе указанные проблемы возникают не всегда, а когда возникают, их можно решить умеренным усложнением плэйбка. Приведу пример из рабочего плэйбука для установки Sonatype Nexus Repository Manager.
# Отдельным таском выкладываем файлы при изменении которых нужно перезапустить сервис. # Т.к. в файлах есть секреты, то отключаем вывод change. - name: Copy nexus files ansible.builtin.template: src: 'files{{ item }}' dest: '{{ item }}' owner: root group: nexus mode: '0640' loop: - /opt/sonatype-work/nexus3/etc/nexus.lic - /opt/sonatype-work/nexus3/etc/nexus.properties - /opt/sonatype-work/nexus3/etc/nexus-secret.json - /opt/sonatype-work/nexus3/etc/fabric/nexus-store.properties - /opt/nexus/bin/nexus.vmoptions notify: - Restart nexus.service no_log: true # Так же отдельным таском скопируем ключи и сертификаты nginx - name: Copy nginx TLS certificates ansible.builtin.template: src: '{{ item.src }}' dest: '/{{ item.path }}' owner: root group: root mode: '0600' with_community.general.filetree: files/ when: item.state == 'file' and item.path is search('etc/nginx/ssl') notify: - Reload services no_log: true # Далее как обычно копируем все остальные файлы, не забывая при этом во when # пропустить копирование тех, содержимое которых не должно попасть в change. - name: Copy config files # noqa: risky-file-permissions ansible.builtin.template: src: '{{ item.src }}' dest: '/{{ item.path }}' with_community.general.filetree: files/ when: item.state == 'file' and item.path is not search('etc/nginx/ssl') and item.path is not search('nexus-store.properties') and item.path is not search('nexus-secret.json') notify: - Reload services
И на этом всё. Надеюсь материал будет для вас полезным. Если обнаружите ошибки/опечатки, то сообщите, пожалуйста, в лс. Всем IaC и passed-пайплайнов!
ссылка на оригинал статьи https://habr.com/ru/articles/856762/
Добавить комментарий