Мой базовый паттерн использования ansible
Всем привет! Сегодня я хочу поделиться с вами простым паттерном использования 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: relod_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-пайплайнов!