Как управлять многостадийной средой развертывания с помощью системы Ansible

Вступление

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

В этом руководстве мы обсудим несколько стратегий применения Ansible для работы с многостадийными средами развертывания. Как правило, различные требования для разных стадий приводят к необходимости работать с различным количеством конфигурацией и компонентов. К примеру, требования к памяти сервера развертывания могут отличаться от аналогичных требований, предъявляемых на этапах переноса и производства, в связи с чем очень важно четко контролировать приоритезацию переменных, которые соответствуют подобным требованиям. В этой статье мы поговорим о методах, которые помогут абстрагировать подобные различия, а также о некоторых конструкциях, посредством которых система Ansible поддерживает повторное использование конфигураций.

Неполные стратегии для управления многостадийными средами с помощью Ansible

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

В рамках настоящего руководства мы рассмотрим подход, основанный на переменных группы и множественных инвентарях Ansible. Однако доступные возможности на этом не ограничиваются, и в системе существует несколько других стратегий, которые также заслуживают внимания. Ниже мы поговорим о некоторых подобных идеях и о том, какие проблемы и почему могут возникнуть при реализации подобных идей в сложных комплексных средах.

Если вы хотите начать с рекомендованной стратегии Ansible, просто пролистайте настоящее руководство вниз до раздела, посвященного применению групп и множественных инвентарей Ansible.

Метод, основанный исключительно на переменных группы

На первый взгляд может показаться, что переменные группы позволяют добиться разделения сред, необходимого для работы в Ansible, без использования каких-либо дополнительных приемов. Вы можете обозначить те или иные серверы как принадлежащие к вашей среде разработке, в то время как другие можно отнести к зонам переноса и производства. Ansible позволяет с легкостью создавать группы и присваивать им различные переменные.

Однако пересечение между группами может привести к возникновению серьезных проблем в данной системе. Группы часто используются для категоризации сразу нескольких измерений. К примеру:

  • Среды развертывания (локальные (local), разработка (dev), перенос (stage), производство (prod) и так далее);
  • Функционал хоста (веб-службы, серверы базы данных и так далее);
  • Регион центра обработки данных (NYC, SFO и так далее)

В таком случае хосты, как правило, определяются в одну группу для каждой категории. К примеру, хост может одновременно являться веб-службой (функционал) на стадии переноса (среда развертывания) в регионе NYC (регион центра обработки данных).

Если для того или иного хоста одна и та же переменная задана одновременно для нескольких групп, система Ansible не сможет предоставить вам каких-либо достаточно точных инструментов для определения приоритетности. Например, если вы хотите задать переменным, связанным со средой развертывания, более высокий приоритет, чем у всех прочих переменных, система Ansible не сможет предложить вам необходимый функционал, чтобы прописать это условие.

Вместо этого, Ansible будет использовать последнее загруженное значение. Поскольку Ansible оценивает группы в алфавитном порядке, наибольший приоритет будет отдан тем переменным, имя группы которых начинается с буквы, идущей последней по алфавиту. Хотя поведение системы в данном случае весьма предсказуемо, управление алфавитным порядком имен групп – это далеко не самый эффективный метод с точки зрения администратора.

Использование дочерних групп для установления иерархии

Система Ansible позволяет пользователю присваивать одни группы другим группам при помощи синтаксиса [groupname:children] в инвентаре. Это дает вам возможность сделать одни группы членами других групп. Такие дочерние группы имеют свойство переопределять переменные, заданные родительскими группами.

Как правило, данная особенность используется для естественной классификации. К примеру, у нас есть группа под названием environments, которая включает в себя группы dev, stage, prod. Это значит, что мы можем задать переменные в группе environment, которые будут иметь более низкий приоритет, чем переменные группы dev. Аналогично вы можете создать родительскую группу под названием functions, которая будет включать в себя группы web, database и loadbalancer.

Однако такой подход не решает проблему пересечения групп, поскольку дочерние группы переопределяют только своих «родителей». Да, переменные дочерней группы могут переопределять переменные родительской, но подобная организация не устанавливает каких-либо связей между категориями групп, к примеру, environments и functions. Приоритет переменных данных груп относительно друг друга до сих пор остается незаданным.

Вы можете использовать подобную систему в своих целях, установив противоестественную принадлежность групп.  К примеру, если вы хотите задать следующую приоритетность групп в порядке убывания приоритета:

  • Среда развертывания;
  • Регион;
  • Функционал;

Вы можете присвоить группам принадлежность друг другу, которая будет иметь следующий вид:

Пример инвентаря

. . .
[function:children]
web
database
loadbalancer
region
[region:children]
nyc
sfo
environments
[environments:children]
dev
stage
prod

Таким образом, мы установили иерархию, которая позволяет переменным, отвечающим за регион, переопределять переменные, отвечающие за функционал, поскольку группа region является дочерней группой группы function. Аналогично, переменные, заданные для группы environments переопределяют все прочие переменные. Это значит, что мы присвоим одной и той же переменной различные значения в группах dev, nyc и web, хост, принадлежащий к каждой из них, будет использовать переменную из группы dev.

Такая методика позволяет достичь желаемого результата и обеспечивает достаточно предсказуемое поведение системы. Однако она является достаточно сложной и запутанной и создает путаницу, связанную с потребностью отличать истинные дочерние группы и дочерние группы, необходимые для установления иерархии друг от друга. Система Ansible специально предназначена для того, чтобы обеспечить простую и удобную работу с конфигурациями даже для начинающих пользователей, и подобные «обходные пути» ставят под сомнение ее основную идею. 

Применение конструкций Ansible, которые обеспечивают четкий порядок загрузки

В системе Ansible существуют несколько конструкций, которые обеспечивают возможность установить четкий порядок загрузки переменных, а именно vars_files и include_vars. Эти конструкты могут использоваться в инструкциях Ansible для того, чтобы установить четкий порядок загрузки дополнительных переменных в рамках последовательности, установленной файлом. Команда vars_files работает в контексте запусков, в то время как модуль include_vars может применяться в задачах.

Основной идеей данного принципа является установка единственных базовых идентификационных переменных для group_vars и их последующее использование для загрузки корректных файлов переменных в отношении остатка желаемых переменных.

К примеру, несколько файлов group_vars могут иметь следующий вид:

group_vars/dev

---
env: dev

group_vars/stage

---
env: stage

group_vars/web

---
function: web

group_vars/database

---
function: database

В рамках данного примера мы имеем отдельный файл переменных, который определяет важные переменные для каждой группы. Для большей ясности они, как правило, хранятся в отдельной директории vars. В отличии от файлов group_vars, при работе с модулем include_vars файлы должны иметь расширение .yml.

Предположим, что нам нужно задать переменной server_memory_size различные значения в каждом файле vars. Наверняка, ваш сервер разработки имеет меньший объем памяти, чем сервер производства. Более того, ваши серверы сетевых служб и баз данных также, вероятно, имеют разные требования к памяти:

vars/dev.yml

---
server_memory_size: 512mb

vars/prod.yml

---
server_memory_size: 4gb

vars/web.yml

---
server_memory_size: 1gb

vars/database.yml

---
server_memory_size: 2gb

В таком случае мы можем создать набор инструкций, который устанавливает четкий порядок загрузки правильного файла vars на основании значений, присвоенных хосту из файла group_vars. Порядок загрузки файлов определяет приоритетность, где последнее загруженное значение имеет наибольший приоритет.

Рассмотрим пример инструкции с функционалом vars_files, которая будет иметь следующий вид example_play.yml:

---
- name: variable precedence test
  hosts: all
  vars_files:
    - "vars/{{ env }}.yml"
    - "vars/{{ function }}.yml"
  tasks:
    - debug: var=server_memory_size

Поскольку группы, отвечающие за функционал, загружаются последними, значение server_memory_size будет взято из файлов var/web.yml и var/database.yml:

ansible-playbook -i inventory example_play.yml
. . .
TASK [debug] *******************************************************************
ok: [host1] => {
    "server_memory_size": "1gb"      # value from vars/web.yml
}
ok: [host2] => {
    "server_memory_size": "1gb"      # value from vars/web.yml
}
ok: [host3] => {
    "server_memory_size": "2gb"      # value from vars/database.yml
}
ok: [host4] => {
    "server_memory_size": "2gb"      # value from vars/database.yml
}
. . .

Если мы изменим порядок загрузки файлов, мы можем придать наивысший приоритет переменным, отвечающим за среду развертывания example_play.yml:

---
- name: variable precedence test
  hosts: all
  vars_files:
    - "vars/{{ function }}.yml"
    - "vars/{{ env }}.yml"
  tasks:
    - debug: var=server_memory_size

Повторный запуск набора инструкций показывает, что теперь значения берутся из файлов, отвечающих за среду развертывания:


ansible-playbook -i inventory example_play.yml
    . . .
TASK [debug] *******************************************************************
ok: [host1] => {
    "server_memory_size": "512mb"      # value from vars/dev.yml
}
ok: [host2] => {
    "server_memory_size": "4gb"        # value from vars/prod.yml
}
ok: [host3] => {
    "server_memory_size": "512mb"      # value from vars/dev.yml
}
ok: [host4] => {
    "server_memory_size": "4gb"        # value from vars/prod.yml
}
. . .

Эквивалентный набор инструкций, составленный с использованием модуля include_vars, работающий по принципу задачи, будет иметь следующий вид:

---
- name: variable precedence test
  hosts: localhost
  tasks:
    - include_vars:
        file: "{{ item }}"
      with_items:
        - "vars/{{ function }}.yml"
        - "vars/{{ env }}.yml"
    - debug: var=server_memory_size

Это единственная зона, где Ansible позволяет задать четкий порядок загрузки, что может оказаться весьма и весьма полезным. Однако, как и в случае предыдущего примера, такая методика имеет ряд существенных недостатков.

Во-первых, использование функций vars_files и include_vars вынуждает пользователя размещать переменные, тесно привязанные к группам, в разных локациях. Локация group_vars становится фиктивным модулем для фактических переменных, которые содержатся в директории vars. Это опять-таки добавляет системе сложности и делает ее более запутанной. Пользователю приходится привязывать правильные файлы переменных к хосту, что система Ansible обычно делает автоматически при помощи команды group_vars.

Что еще более важно, полагаясь на подобные техники, вы возводите их в разряд обязательных. В таком случае в каждом наборе инструкций придется прописывать раздел, который отвечает за определение четкого порядка загрузки правильных файлов в правильной последовательности. Наборы инструкций, не содержащие подобный раздел, не смогут использовать связанные переменные. Более того, запуск команды ansible для ситуационных задач станет практически невозможным для всех инструкций и задач, которые основываются на переменных.

Рекомендованная стратегия Ansible: Использование групп и множественных инвентарей

Итак, ранее мы уже рассмотрели несколько стратегий для управления многостадийными средами и обсудили причины, которые не позволяют использовать их в качестве полного оптимального решения. Однако проект Ansible предлагает своим пользователям несколько полезных рекомендаций касательно того, как лучше всего абстрагировать вашу инфраструктуру по разным средам.

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

Базовая структура директории будет иметь следующий вид:

.
├── ansible.cfg
├── environments/         # Parent directory for our environment-specific directories
│   │
│   ├── dev/              # Contains all files specific to the dev environment
│   │   ├── group_vars/   # dev specific group_vars files
│   │   │   ├── all
│   │   │   ├── db
│   │   │   └── web
│   │   └── hosts         # Contains only the hosts in the dev environment
│   │
│   ├── prod/             # Contains all files specific to the prod environment
│   │   ├── group_vars/   # prod specific group_vars files
│   │   │   ├── all
│   │   │   ├── db
│   │   │   └── web
│   │   └── hosts         # Contains only the hosts in the prod environment
│   │
│   └── stage/            # Contains all files specific to the stage environment
│       ├── group_vars/   # stage specific group_vars files
│       │   ├── all
│       │   ├── db
│       │   └── web
│       └── hosts         # Contains only the hosts in the stage environment
│
├── playbook.yml
│
└── . . .

Как видите, каждая среда имеет четко определенные границы и изолирована от всех остальных. Директории среды содержат файл инвентаря (произвольно названные hosts) и отдельную директорию group_vars.

При этом мы наблюдаем очевидное дублирование в дереве директории. Для каждой отдельной среды имеются свои файлы web и db. В таком случае подобное дублирование является желательным. Изменения переменных можно реализовать во всех средах, для чего необходимо сначала изменить сами переменные в одной среде, после чего перенести их в следующую среду после тестирования, точно так же, как это делается с изменениями кода или конфигурации. Переменные group_vars отслеживают текущие настройки по умолчанию для каждой среды. 

Единственным ограничением является невозможность выбрать все хосты во всех средах посредством единой функции. К счастью, это ограничение относится к той же категории, что и проблема с дублированием переменных выше. Хотя иногда бывает достаточно полезно выбрать для той или иной задачи все ваши веб-сервера, в подавляющем большинстве случаев вы все же предпочтете этому возможность одновременно реализовывать необходимые изменения во всех средах. Это позволяет предотвратить негативное влияние различных ошибок на вашу среду производства.

Как задать перекрестные переменные в разных средах? 

Единственная вещь, которую невозможно выполнить в рамках рекомендованного подхода, это обмен переменными между разными средами. Существует несколько способов, посредством которых мы можем реализовать обмен переменными между средами. Наиболее простым из них является использование свойства системы Ansible работать с директориями вместо отдельных файлов. Мы можем заменить файл all в каждой директории group_vars на директорию all.

Внутри директории мы снова можем задать файл с переменными, специфическими для данной конкретной среды. Затем мы можем создать символическую связь с местом нахождения файла, который содержит перекрестные переменные для разных сред. Оба указанных файла будут применяться ко всем хостам в рамках соответствующей среды.

Начнем с создания файла перекрестных переменных где-нибудь в нашей иерархии. К примеру, давайте поместим подобный файл в директорию environments. Теперь разместим в этом файле перекрестные переменные:

cd environments
touch 000_cross_env_vars

Дальше переходим в одну из директорий group_vars, переименовываем файл all и создаем директорию all. Перемещаем переименованный файл в новую директорию:

cd dev/group_vars
mv all env_specific
mkdir all
mv env_specific all/

Теперь вы можете создать символическую связь с файлом, содержащим перекрестные переменные:

cd all/
ln -s ../../../000_cross_env_vars .

После того как вы выполнили шаги, описанные выше, для каждой вашей среды, структура вашей директории будет иметь следующий вид:

├── ansible.cfg
├── environments/
│   │
│   ├── 000_cross_env_vars
│   │
│   ├── dev/
│   │   ├── group_vars/
│   │   │   ├── all/
│       │   │   ├── 000_cross_env_vars -> ../../../000_cross_env_vars
│   │   │   │   └── env_specific
│   │   │   ├── db
│   │   │   └── web
│   │   └── hosts
│   │
│   ├── prod/
│   │   ├── group_vars/
│   │   │   ├── all/
│   │   │   │   ├── 000_cross_env_vars -> ../../../000_cross_env_vars
│   │   │   │   └── env_specific
│   │   │   ├── db
│   │   │   └── web
│   │   └── hosts
│   │
│   └── stage/
│       ├── group_vars/
│       │   ├── all/
│       │   │   ├── 000_cross_env_vars -> ../../../000_cross_env_vars
│       │   │   └── env_specific
│       │   ├── db
│       │   └── web
│       └── hosts
│
├── playbook.yml
│
└── . . .

Переменные, заданные в файле 000_cross_env_vars будут доступны для каждой среды с низким приоритетом.

Установка инвентаря среды по умолчанию

Вы можете создать файл инвентаря по умолчанию в файле ansible.cfg. Это может оказаться весьма полезным по нескольким причинам.

Во-первых, это позволит вам забыть о необходимости прописывать четкие флажки инвентарей для ansible и ansible-playbook. Итак, вместо того чтобы прописать:

ansible -i environments/dev -m ping

Вы можете получить доступ к инвентарю по умолчанию, прописав:

ansible -m ping

Во-вторых, установка инвентаря по умолчанию поможет предотвратить случайное влияние нежелательных изменений на среду переноса и среду производства. Привязав инвентарь по умолчанию к вашей среде разработки, вы сможете быть уверены, что любые изменения будут затрагивать лишь наименее важную инфраструктуру. В таком случае для переноса изменений в другую среду потребуется отдельно выполнить конкретное действие, которое требует установки флажка -i.

Для того чтобы задать инвентарь по умолчанию, откройте ваш файл ansible.cfg. Он может находиться в корневой директории вашего проекта либо в /etc/ansible/ansible.cfg в зависимости от вашей конфигурации.

Примечание: Ниже показан пример редактирования файла ansible.cfg в директории проекта. Если вы используете файл /etc/ansibile/ansible.cfg для внесения изменений, необходимо изменить путь редактирования ниже. При использовании /etc/ansible/ansible.cfg, если ваши инвентари содержатся за пределами директории /etc/ansible, убедитесь, что вы используете абсолютный путь, а не относительный путь при установке значения inventory.
nano ansible.cfg

Как уже было сказано выше, в качестве инвентаря по умолчанию рекомендуется задать вашу среду разработки. Обратите внимание, что мы также можем выбрать всю директорию среды вместо файла хостов, который в ней содержится:

[defaults]
inventory = ./environments/dev

Теперь вы сможете использовать инвентарь по умолчанию без опции -i. Все прочие инвентари, кроме инвентаря по умолчанию, по-прежнему будут требовать использования -i, что поможет защитить их от случайных изменений.

Заключение

В настоящей статье мы поближе познакомились с той гибкостью, какую система Ansible предоставляет своим пользователям в контексте управления хостами в разных средах развертывания. Данная особенность системы позволяет пользователям применять множество различных стратегий для управления приоритетностью переменных, когда один и тот же хост является членом разных групп, однако некоторая неоднозначность и отсутствие официальных инструкций по-прежнему могут привести к возникновению определенных трудностей. Как и в случае любой технологии, наиболее оптимальная методика для вашей конкретной организации будет зависеть от конкретного пользователя и сложности ваших требований. Самый лучший способ найти оптимальную стратегию, которая лучше всего подходит под ваши индивидуальные потребности, это проверить все экспериментальным путем. Делитесь вашим опытом работы с системой и подходами, которые вы используете, в комментариях.

Комментарии

0