Если вы давно читаете этот канал, то точно знаете, что я люблю Ansible и использую его для управления инфрой на текущих проектах.
Здесь я хотел рассказать как именно я структурирую inventory-файлы, переменные, роли и как это все запускаю. Может это не супер-бест-практисы, но это работает довольно давно, неплохо поддерживается. У Ansible есть свои минусы, но тут не об этом.
Здесь не будет пошаговых инструкций. Здесь — демонстрация того, как делаю я. Может быть это даст вам каких-нибудь идей, или вы захотите поделиться своим мнением в комментариях в Телеграм-чате.
В интернете over9000 гайдов типа создаете группу хостов web-servers, создаете плейбук, который запускаете на этой группе и устанавливаете nginx. Для базы данных тоже самое. Сколько таких плейбуков и групп получится, если у вас 100 серверов с разным наполнением? Нужно что-то другое.
Роли #
Во-первых: роли. Роль — это набор заданий, параменных, шаблонов которые объединены в отдельный модуль. Например роль для установки и настройки Nginx, роль для управления пользователями, роль для установки и настройки PostgreSQL.
Роль должна выполнять одну задачу.
Роль должна быть универсальной: мы можем запустить ее на любом хосте и она сделает то, что должна.
С помощью переменных в роль можно передавать различные настройки.
В основном все роли я пишу сам, а не использую готовые из коммьюнити. Мне кажется, что коммьюнити-роли перегружены дополнительными параметрами. Я могу взять что-нибудь интересное из них, это да. Но в основном — пишу сам под свои требования.
Inventory #
Во-вторых: inventory. Про него я уже писал. Вот тут. Инвентарь — это файлы, где мы описываем нашу инфраструктуру. Базово — это список групп и хостов, на которых будут запускаться наши плейбуки. Но так же в inventory мы можем указывать переменные для наших хостов. Тут и начинается iNfRaStRuCtUrE aS cOdE.
Напомню структуру инвентори здорового человека:
ansible/inventories
├── dev
│ ├── dev.yml
│ ├── group_vars
│ │ ├── all.yml
│ └── host_vars
│ ├── server00dev.et.yml
│ └── frontend01dev.et
│ ├── common.yml
│ ├── logrotate.yml
│ └── promtail.yml
├── dwh_cluster
│ ├── dwh.yml
│ ├── group_vars
│ │ ├── all.yml
│ │ ├── dwh_cluster.yml
│ │ └── dwh_cluster_wo_dwh00.yml
│ └── host_vars
│ ├── dwh00.et
│ │ ├── backups.yml
│ │ ├── blackbox-exporter.yml
│ │ └── common.yml
│ ├── dwh01.et.yml
│ └── dwh50.et.yml
...
Я держу поддиректории под окружения. В каждой поддиректории есть .yml
файл, который содержит только список хостов. Максимум туда добавляем настройки подключения, например ansible_host
, etc…:
all:
children:
server00:
hosts:
server00dev.et:
Есть директории host_vars
и group_vars
в которых хранятся файлы с названиями хостов из инвентори, в них находятся все переменные-настройки хостов. Так же если host_var
-файл разрастается и становится сложно его поддерживать, его можно превратить в директорию с несколькими файлами (см. выше в примере).
Что в host_var-файле у меня есть интересного? Одна из самых важных, на мой взгляд, переменных:
host_roles:
- openjdk
- promtail
- haproxy
- postgresql
Она указывает КАКИЕ РОЛИ нужно запустить на ЭТОМ хосте.
Так же есть, например, переменная users
, в которой я перечисляю дополнительных пользователей, которых нужно создать на сервере. В этом же файле я указываю “настройки” для моих ролей, например:
postgresql_version: 16
postgresql_global_config_options:
- { option: listen_addresses, value: '0.0.0.0' }
- { option: max_connections, value: '500' }
или
iptables_additional_rules:
- -A INPUT -s 172.16.0.0/12 -m comment --comment "allow from docker containers" -j ACCEPT
- -A INPUT -s 10.40.12.0/24 -m comment --comment "allow from vpn network" -j ACCEPT
В последнее время я иду к тому, что в inventory указываю полноценные конфиги для сервисов, например для promtail. Прямо внутри переменной описываю конфигурацию, а роль уже добавит этот конфиг к дефолтному:
promtail_config_scrape_configs:
- job_name: backend_dev_rest
static_configs:
- targets:
- localhost
labels:
job: backend_dev_rest
...
Это позволяет мне иметь при необходимости уникальный (кастомный) конфиг для какого-либо сервиса вместо оригинально (или в дополнение к нему). Например на всех хостах у меня есть дефолтный конфиг promtail - он указан в roles/promtail/defaults
, а в host_vars
я его дополняю.
Плейбук #
В-третьих: как это запускать?
У меня есть “общий” плейбук common.yml
, в котором по сути есть всего один таск include_role
. В этот таск предается список ролей содержащий:
- обязательные роли, которые должны быть прокатаны на всех хостах, например
node_exporter
,zabbix_agent
, настройки ssh, установка правильного хостнейма, настройки DNS, NTP и так далее. - роли из переменной
host_roles
Так как в host_roles у меня лежит список (тип данных) ролей, то этот список просто расширит уже имеющийся список дефолтных. Я добавляю jinja-фильтр default([])
, чтобы смержить туда пустой список, если переменная host_roles
вообще не задана, иначе будет ошибка. Итогово это выглядит так:
- name: All hosts playbook
hosts: all
become: true
gather_subset: ['default_ipv4', 'distribution_release']
ignore_unreachable: false
tasks:
- name: Include roles
ansible.builtin.include_role:
name: "{{ item }}"
loop:
- common_software
- users
- "{{ host_roles | default([]) }}"
- "{{ group_roles | default([]) }}"
- ntp
- node-exporter
- zabbix_agent
- sshd
Теперь, если я хочу настроить сервер с нуля (ну, не совсем с нуля, конечно), то я просто запускаю:
ansible-playbook common.yml -i inventories/dev -l server00dev.et -D
И он прокатится только на одном сервере. Уберу -l server00dev.et
, и плейбук запустится на всех дев-серверах.
Автоматизация #
В-четвертых: как это автоматизировать? Это немного не укладывается в рамки стандартного CI/CD (где сборка/загрузка/деплой) - я скорее про автоматическую раскатку этого поделия на серверы.
Предположим, что мы описали и добавили все наши серверы в inventory, разделили его на окружения: inventories/dev/dev.yml
, inventories/dev/prod.yml
. В host_vars и group_vars добавили нужные нам роли для хостов, конфиги и параметры для сервисов. Как будем это запускать?
Весь инфраструтурный код я храню в отдельном репозитории, который гордо обозвал ansible
. Я хочу, чтобы при изменении кода у меня автоматически запускался ansible-playbook common.yml
.
Я себе выбрал такие условия:
- если меняется что-то в
inventories/dev
, то запустить common.yml с параметром -i inventories.dev - аналогично для prod-окружения
Так как я использую Gitlab, то у меня есть .gitlab-ci.yml
файл. В нем я описываю условия, которые стриггерят запуск плейбука:
deploy_dev:
stage: deploy
before_script:
- mkdir -p ~/.ssh
- echo "$ADMIN_SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- ansible-playbook -i inventories/dev common.yml --diff
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes:
- 'inventories/dev/**/*'
- 'roles/node-exporter/**/*'
- 'roles/users/**/*'
...
Таким образом этот пайплайн запустится автоматически в случае изменений в файлах под inventories/dev
, а так же при изменениях в некоторых ролях — например я глобально обновил версию node-exporter в defaults
.
Общий workflow (реальный и идеальный) #
- Инженер клонирует себе репозиторий
- Создает отдельную ветку, вносит необходимые изменения.
- Проверяет через check-mode со своего компьютера — вот этот пункт - плохой паттерн, так как он требует доступа на сервера, и может вызывать configuration drift. Проверку должен выполнять CI.
- Пушит код в Gitlab, создает MR.
- В Gitlab проходят тесты (идеальный вариант), другой инженер проводит код-ревью.
- MR мержится в master-ветку
- Gitlab запускает соответствующий пайплайн, чтобы привести инфрастркутуру к целевому состоянию
Что надо добавлять? #
- Тестирование. Как минимум — прогон через check-mode. Как идеал — тестирование ролей на временной инфрастркутуре.
- Линтеры. Нужно прогонять код, чтобы он соответствовал правильному синтаксису.
Было полезно? Может появились какие-то идеи? Или вы делаете у себя в инфрастркутуре по-другому? Поделись этим в 👉 телеграм-чате! Мне очень интересно было бы почитать.