В в прошлых статьях были описаны обшие концепции и управляющие конструкции.
Так как часто на серверах есть потребность не в простом копировании файлов, как они есть, а необходимость создать файл под конкретный сервер. Для этого и служат шаблоны.
Шаблоны
Шаблон это файл формата Jinja Template, которые обычно имеют разрешение j2. Детальная информация о шаблонах доступна в документации. Для работы шаблонов используется модуль template. Пример задачи с модулем шаблон:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
--- - name: Template config J2 hosts: all become: yes vars: source_file: ./configs/server.j2 dest_path: /etc/server.conf tasks: - name: Generate config template: src={{ source_file }} dest= {{ dest_path }} |
Берется исходный файл взятый в качестве шаблона в нем отрабатывается алгоритм преобразования и сохраняется по указанному пути. Также происходит проверка что файл уже есть, как и с копированием, а также выполнение обработчиков в случае если копирование произошло.
При этом шаблон будет иметь следующий вид:
1 2 3 4 |
[server] ip={{ ansible_default_ipv4.address }} name={{ ansible_host_name }} owner={{ owner }} |
Важно отметить, что это файл любого формата, как то конфигурационный страница html в оторой вставляются управляющие конструкции jinja, которые при формирования файла заменяются на значения взятые из ansible.
При работе с переменными есть несколько особенностей. Например возможно задать значение по умолчанию для переменной, если она не будет определена.
1 |
<span class="cp">{{</span> <span class="nv">some_variable</span> <span class="o">|</span> <span class="nf">default</span><span class="o">(</span><span class="m">5</span><span class="o">)</span> <span class="cp">}}</span> |
Если перменная some_variable не определена, то будет взято значение 5.
1 |
<span class="cp">{{</span> <span class="nv">lookup</span><span class="o">(</span><span class="s1">'env'</span><span class="o">,</span> <span class="s1">'MY_USER'</span><span class="o">)</span> <span class="o">|</span> <span class="nf">default</span><span class="o">(</span><span class="s1">'admin'</span><span class="o">,</span> <span class="kp">true</span><span class="o">)</span> <span class="cp">}}</span> |
Если необходимо, что бы значение по умолчанию задавалось даже в том случае если переменная вычисляется как пустая строка или false, то в этом случае к default добавляется второй параметр true.
При обработке шаблона необходимо определение всех переменных или задание им значений по умолчанию, если это не так, то будет ошибка. Однако ситуация может потребовать что бы переменная была не обязательной, тогда применяется default(omit).
1 |
<span class="nt">mode</span><span class="p">:</span> <span class="s">"</span><span class="cp">{{</span> <span class="nv">item.mode</span> <span class="o">|</span> <span class="nf">default</span><span class="o">(</span><span class="nv">omit</span><span class="o">)</span> <span class="cp">}}</span><span class="s">"</span> |
Есть разные другие фильтры и преобразователи как то to_json, to_yaml, to_nice_json, to_nice_yaml, zip, zip_longest
Вот пример из документации:
1 2 3 4 5 |
<span class="p p-Indicator">-</span> <span class="nt">name</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">Give me longest combo of three lists , fill with X</span> <span class="nt">ansible.builtin.debug</span><span class="p">:</span> <span class="nt">msg</span><span class="p">:</span> <span class="s">"</span><span class="cp">{{</span> <span class="o">[</span><span class="m">1</span><span class="o">,</span><span class="m">2</span><span class="o">,</span><span class="m">3</span><span class="o">]</span> <span class="o">|</span> <span class="nf">zip_longest</span><span class="o">([</span><span class="s1">'a'</span><span class="o">,</span><span class="s1">'b'</span><span class="o">,</span><span class="s1">'c'</span><span class="o">,</span><span class="s1">'d'</span><span class="o">,</span><span class="s1">'e'</span><span class="o">,</span><span class="s1">'f'</span><span class="o">],</span> <span class="o">[</span><span class="m">21</span><span class="o">,</span> <span class="m">22</span><span class="o">,</span> <span class="m">23</span><span class="o">],</span> <span class="nv">fillvalue</span><span class="o">=</span><span class="s1">'X'</span><span class="o">)</span> <span class="o">|</span> <span class="nf">list</span> <span class="cp">}}</span><span class="s">"</span> <span class="c1"># => [[1, "a", 21], [2, "b", 22], [3, "c", 23], ["X", "d", "X"], ["X", "e", "X"], ["X", "f", "X"]]</span> |
Кроме того весьма интересные примеры извлечения значений из JSON данных:
1 2 3 4 |
<span class="p p-Indicator">-</span> <span class="nt">name</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">Display all server names</span> <span class="nt">ansible.builtin.debug</span><span class="p">:</span> <span class="nt">var</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">item</span> <span class="nt">loop</span><span class="p">:</span> <span class="s">"</span><span class="cp">{{</span> <span class="nv">domain_definition</span> <span class="o">|</span> <span class="nf">community</span><span class="nv">.general.json_query</span><span class="o">(</span><span class="s1">'domain.server[*].name'</span><span class="o">)</span> <span class="cp">}}</span><span class="s">"</span> |
Другой вариант применения является посроение другого запроса:
1 2 3 4 5 6 |
<span class="p p-Indicator">-</span> <span class="nt">name</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">Display all ports from cluster1</span> <span class="nt">ansible.builtin.debug</span><span class="p">:</span> <span class="nt">var</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">item</span> <span class="nt">loop</span><span class="p">:</span> <span class="s">"</span><span class="cp">{{</span> <span class="nv">domain_definition</span> <span class="o">|</span> <span class="nf">community</span><span class="nv">.general.json_query</span><span class="o">(</span><span class="nv">server_name_cluster1_query</span><span class="o">)</span> <span class="cp">}}</span><span class="s">"</span> <span class="nt">vars</span><span class="p">:</span> <span class="nt">server_name_cluster1_query</span><span class="p">:</span> <span class="s">"domain.server[?cluster=='cluster1'].port"</span> |
Есть еще интересная функция urlsplit, которая разделяет url на части, при этом можно получить как комплексный объект в котором есть все части, так и можно получить конкретную часть:
1 |
{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('hostname') }} # => 'www.acme.com' |
Так же есть способы работать с регулярными выражениями:
1 |
# Extracts the database name from a string <br />{{ 'server1/database42' | regex_search('database[0-9]+') }} # => 'database42' <br /><br /># Example for a case insensitive search in multiline mode <br />{{ 'foo\nBAR' | regex_search('^bar', multiline=True, ignorecase=True) }} <br /># => 'BAR' <br /><br /># Extracts server and database id from a string <br />{{ 'server1/database42' | regex_search('server([0-9]+)/database([0-9]+)', '\\1', '\\2') }} <br /># => ['1', '42'] <br /><br /># Extracts dividend and divisor from a division <br />{{ '21/42' | regex_search('(?P<dividend>[0-9]+)/(?P<divisor>[0-9]+)', '\\g<dividend>', '\\g<divisor>') }} <br /># => ['21', '42'] |
Если регулярное выражение ничего не находит, то возвращается пустая строка. Не менее важно наличие еще и функции замены regex_replace, regex_findall.
Есть еще функции связанные с получением частей пути, как то имя файла, каталога или диск. Это функции dirname, win_dirname, basename, win_basename, win_splitdrive.
Есть фильтры по работе со строками join, split, b64encode, b64decode, quote – соотвественно соединение значений, разбиение строки преобразование в base64 и обратно, а также экранирование.
Роли
Есть еще одна технология ролей (смотри документацию), которая позволяет создавать переносисые шалоны для повтороного использования, вместе с файламт которые необходимы.
В каталоге /etc/ansible создаем каталог roles. Потом в этом каталоге вызваем команду ansible-galaxy init <имя роли>. В результате создатся структура каталогов внутри каталога с именем роли. Структура показана на картинке ниже:

Сами создаваемые файлы пустые. Их стурктура имеет следующие значение:
-
tasks/main.yml
– основной список задач которые выполняются в роли. -
handlers/main.yml
– обработчики, которые используются внутри или снаружи роли. -
library/my_module.py
– модули, которые используются в данной роли (см. Embedding modules and plugins in roles для более полной инфорации). -
defaults/main.yml
– переменные по умолчанию для роли (см Using Variables для большей информации). Эти переенные имеют наиболее низкий приоритет чем любые другие переменные и могут быть легко переопределены другими переменными даже темы, что заданы в файле инвентаризации. -
vars/main.yml
– другие переменные для роли (см Using Variables). -
files/main.yml
– файлы которые роль устанавливает. -
templates/main.yml
– шаблоны которые роль устанавливает. -
meta/main.yml
– metadata для роли влючая зависимости.
Таким образом, мы получим разделение плейбука на части, которые будут отнесены в специальные файлы. Возмем плейбук из прошлой части, установка веб сервера. И разнесем его по файлам в соотвествии с назначением.
Так например переменная dst_path будет вынесена в файл vars/main.yml
. (при этом путь к файлам для копирования уже не применяется, так как для файлов отведен специальный каталог files/
). Тоже касается и шаблонов для них тоже есть спец каталог – templates/
. А обработчики перенесутся в handlers/main.yml
. В тоже время все задачи переедут tasks/main.yml
.
Таким образом мы получаем файл vars/main.yml
будет иметь вид:
1 2 |
--- dest_path: /var/www/ |
Таким образом мы получаем файл handlers/main.yml
будет иметь вид:
Таким образом мы получаем файл tasks/main.yml
будет иметь вид:
template: src=index.j2 dest= {{ dest_path }}
Получим такус
Что стоит отметить, остуствие путей к файлам, в шаблонах и при копировании, ansible сам знает где брать файлы (они ищутся в соотвествующих каталогах). Происходит такое преобразование:
Для того, что бы использовать роль, нужно указать ее в каком то плейбук:
1 2 3 4 5 6 7 8 |
--- - name: Setup WEB service hosts: all become: yes roles: - <имя роли> - { role: deploy_web_server, when: ansible_system == 'Linux' } |
При этом можно назначить множество ролей. Кроме того, роль можно добавить по условию, как вторая роль в примере.
Информация про роли ищется в каталоге roles, который расположен рядом с файлом плейбук, в пути указанном в файле конфигурации ansible, переменная roles_path, которая имеет значение по умолчанию “~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles”, а также рядом с самим файлом плейбук.
Внешние переменные
Плейбук в примере выше всегда выполняется на всех сервера, и что бы сменить сервера для выполнения, придется изменить файл, сохранить его и снова выполнить. Что не удобно, но такое поведение можно изменить, для этого вместа значения all можно использовать переменную:
ansible-playbook playbook.yml –extra-var “MYSERVERS=all”
Или другой вариант:
ansible-playbook playbook.yml -e “MYSERVERS=all owner=denis”
ВАЖНО: переменные extra-var имеют наивысший приоритет и перезаписывают любые значения, не важно где они были определены.
Include и Import
Посмтрим следующий плейбук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
--- - name: Setup WEB service hosts: all become: yes - vars: mytest: "hellow" - tasks: - name: Test ping: - name: Create folder path: /home/user/dir1 state: direcotry mode: 0755 - name: Create folder path: /home/user/dir2 state: direcotry mode: 0755 - name: Create file copy: dest: /home/user/dir1/file0 content: | Test line 1 Test line 2 {{ mytest }} Test line 3 - name: Create file copy: dest: /home/user/dir2/file1 content: | Test line 1 Test line 2 {{ mytest }} Test line 3 |
В некоторых случаях такой файл может быть очень большим, его может быть удобно разделить на части. Например вынести часть создания файлов в одной место, а создание каталогов в другое.
Например вынесем в отдельный файл create_directory.yml следующего содержимого:
1 2 3 4 5 6 7 8 9 10 |
--- - name: Create folder path: /home/user/dir1 state: direcotry mode: 0755 - name: Create folder path: /home/user/dir2 state: direcotry mode: 0755 |
А содержимое создания файлов в другой файл create_file.yml такого содержимого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--- - name: Create file copy: dest: /home/user/dir1/file0 content: | Test line 1 Test line 2 {{ mytest }} Test line 3 - name: Create file copy: dest: /home/user/dir2/file1 content: | Test line 1 Test line 2 {{ mytest }} Test line 3 |
Тогда главный файл будет иметь следующее содержимое:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--- - name: Setup WEB service hosts: all become: yes - vars: mytest: "hellow" - tasks: - name: Test ping: - name: Create folders include: create_directory.yml - name: Create folders import: create_file.yml |
Основное отличие import от include, в том, что import обрабатывается при парсинге и замещается импортированным содержимым, причем в нем сразу замещаются переменные их значениями. А include вставляется только в момеент выполнения таска и тогда переменные получат значения на момент выполнения (а не описания, как с import). Атрибут name можно не использовать. Можно также передавать переменные:
1 |
import: create_file.yml mytext="new test" |
Эти директивы могут быть использованы и совместно с ролями.