Введение

Что такое Ansible

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

Ansible – это не единственный инструмент в своём роде. Есть Fabric, pyinfra, Nix и, наверное, много других.

Зачем еще одно обучение

Чтобы начать использовать Ansible, достаточно одного файла. Тебе не нужно всё хитроумно раскладывать по папочкам и пытаться сделать красиво с первого раза. Позволь Ansible развиваться вместе с тобой. Если это звучит не убедительно, то просто взгляни на 22 уровня приоритетов переменных. Какой смысл запоминать и использовать их все сразу?

Когда я погружался в официальный User Guide на сайте документации Ansible, я был удивлён, как всё запутано и нагромождено.

Для меня классическое обучение – это когда:

  • определена цель
  • можно следовать каким-то шагам
  • и затем получить какой-то результат

Ничего этого в официальной документации я не нашёл.

Раздел Getting Started подразумевает какую-то подготовку:

  • откуда мне взять IP-адреса тачек?
  • что такое инвентарь?
  • причем тут python?
  • из какого места мне нужно запускать эти команды?

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

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

Как пользоваться этим обучением

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

Первый, второй и третий разделы максимально последовательны, поэтому пропускать какой-либо из них я не советую.

В процессе мы поднимем несколько Docker-контейнеров и позапускаем на них команды.

А в результате у тебя будет справочная папка со скриптами, которые можно адаптировать под свои нужды.

Если застрял

Все файлы, упомянутые в обучении, лежат в репозитории этой книги.

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

Подготовка

Для этого раздела были использованы следующие материалы:

Определения

Control node - любой компьютер, на котором установлен Ansible. С него можно запускать команды и плейбуки.

Managed nodes - устройства/серверы в сети, которыми можно управлять с помощью Ansible. Иногда называют "хост". На них Ansible не установлен.

Inventory - список, управляемых хостов/нод. Иногда инвентарь называют хостфайлом. Там указывают IP-адреса или hostname cерверов. В инвентаре можно объединять хосты в группы для удобства выкатывания. Инвентарь можно описывать в YAML или INI-формате. Здесь я буду использовать YAML-формат.

Modules - единица кода, которую выполняет Ansible. Может настраивать систему, можно объединять в задачи "tasks", можно выполнять в рамках плейбука.

Tasks - единица действия, которое может выполнить Ansible. Можно выполнять таски разово с помощью ad-hoc команд.

Playbooks - упорядоченный список Plays.

Plays – набор Tasks. Для старта рекомендую ограничиться одним Play и воспринимать Playbook как набор Tasks.

Tasks, Plays и Playbooks пишутся на YAML и могут включать в себя переменные.

Установка и папки

Необходимое

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

  • Docker - автоматизировать мы будем контейнеры, так что нужен инструмент для контейниризации. Конкретно Docker не обязателен, можно использовать любой доступный инструмент, который понимает формат Dockerfile
  • Python - основной способ установки Ansible это pip-пакет

Папки

  • создать папку ansible, в ней будут находиться все конфиги и отправная точка для команд
  • создать папку containers в папке ansible

Примечание: Название папки не принципиально, но нужно будет использовать выбранное на протяжении всего обучения

Ansible

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

Use pip in your selected Python environment to install the Ansible package

python -m venv .venv
source .venv/bin/activate

А дальше сам ansible:

pip install ansible

Подготовка хостов

У нас будет два Debian хоста в виде Docker-контейнеров. Так как Ansible будет ругаться на пароли при подключении по SSH, мы сразу настраиваем авторизацию по ключу. Файлы конфигурации контейнеров взяты практически один-в-один из репозитория Praqma/alpine-sshd.

Создать Dockerfile внутри containers:

FROM debian:12
COPY entrypoint.sh /
RUN apt-get update \
    && apt-get install -y openssh-server python3.11 \
    && mkdir /var/run/sshd\
    && mkdir -p /root/.ssh \
    && chmod 0700 /root/.ssh \
    && ssh-keygen -A \
    && sed -i s/^#PasswordAuthentication\ yes/PasswordAuthentication\ no/ /etc/ssh/sshd_config \
    && chmod +x entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/sbin/sshd", "-D"]

Создать entrypoint.sh рядом с Dockerfile:

#!/bin/sh
if [ -z "${AUTHORIZED_KEYS}" ]; then
  echo "AUTHORIZED_KEYS env variable is not set. It is required to setup ssh access to the containers."
  exit 1
fi
echo "Populating /root/.ssh/authorized_keys with the value from AUTHORIZED_KEYS"
echo "${AUTHORIZED_KEYS}" > /root/.ssh/authorized_keys
# Execute the CMD from the Dockerfile:
exec "$@"

Создать docker-compose.yml внутри containers:

version: "3"

services:
  deb1:
    build: .
    environment:
      AUTHORIZED_KEYS: ${AUTHORIZED_KEYS}
    ports:
      - "2222:22"
  deb2:
    build: .
    environment:
      AUTHORIZED_KEYS: ${AUTHORIZED_KEYS}
    ports:
      - "2223:22"

К этому моменту наша рабочая папка выглядит так:

├── .venv
│   └── ...
└── containers
    ├── Dockerfile
    ├── docker-compose.yml
    └── entrypoint.sh

Установить переменную окружения для ssh-ключей, собрать образы и запустить:

export AUTHORIZED_KEYS=$(cat ~/.ssh/id_rsa.pub)
docker compose up --build

Если всё ок, то в конце вывода будет так:

[+] Running 3/1
 ⠿ Network containers_default   Created
 ⠿ Container containers-deb2-1  Created
 ⠿ Container containers-deb1-1  Created
Attaching to containers-deb1-1, containers-deb2-1
containers-deb2-1  | Populating /root/.ssh/authorized_keys with the value from AUTHORIZED_KEYS env variable ...
containers-deb1-1  | Populating /root/.ssh/authorized_keys with the value from AUTHORIZED_KEYS env variable ...

Теперь можно перейти в сосдений терминал и проверить подключение по ssh:

> ssh root@localhost -p 2222
> ssh root@localhost -p 2223

На всякий случай на время обучения можно в ~/.ssh/config добавить настройку для localhost, чтобы не было проблем с добавлением ключей в known_hosts:

Host localhost
    StrictHostKeyChecking no

Конфиги и инвентарь

Теперь можно начать настраивать скрипты. Нужно перейти из containers/ на уровнеь выше и создать inventory.yml внутри ansible/:

---
debs:
  hosts:
    deb1:
      ansible_port: 2222
      ansible_host: localhost
      ansible_user: root
      ansible_python_interpreter: /usr/bin/python3.7
    deb2:
      ansible_port: 2223
      ansible_host: localhost
      ansible_user: root
      ansible_python_interpreter: /usr/bin/python3.7

Конфигурация Ansible

Создать ansible.cfg:

[defaults]
inventory = inventory.yml

Помимо ссылки на инвентарь я обычно устанавливаю эти переменные:

deprecation_warnings=False
nocows=True
vault_password_file=/path/to/vault/password/file

Подробно про каждую настройку и другие можно почитать здесь Ansible Configuration Settings.

Что должно получиться

├── .venv
│   └── ...
├── containers
│   ├── Dockerfile
│   ├── docker-compose.yml
│   └── entrypoint.sh
├── ansible.cfg
└── inventory.yml

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

Первые команды

Для этого раздела были использованы следующие материалы:

Hello World по-ансибловски

Для проверки подключения к хостам выполнить:

ansible all -m ping

Если всё ок, то вывод будет примерно такой:

deb2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
deb1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Возможно потребуется написать yes для того чтобы добавить отпечатки подключений, если в прошлой части не добавляли StrictHostKeyChecking для localhost.

Пользователи

В нашей конфигурации мы используем root, но это не всегда так, поэтому в доках описан такой сценарий:

# as bruce
> ansible all -m ping -u bruce
# as bruce, sudoing to root (sudo is default method)
> ansible all -m ping -u bruce --become
# as bruce, sudoing to batman
> ansible all -m ping -u bruce --become --become-user batman

Разовые команды - 1

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

Самые распространённые примеры использования разовых команд: перезагрузить сервер, скопировать файлы, устанавливать и удалять пакеты, управлять пользователями и т.п.

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

Самая простая команда:

> ansible deb1 -a "echo hello"
deb1 | CHANGED | rc=0 >>
hello

Тоже самое можно выполнить для группы хостов:

> ansible debs -m shell -a 'echo $TERM'
deb2 | CHANGED | rc=0 >>
screen-256color
deb1 | CHANGED | rc=0 >>
screen-256color

Попробуем скопировать файл инвентаря с нашей машины на удалённые хосты:

> ansible debs -m copy -a "src=inventory.yml dest=/tmp/foo"
deb2 | CHANGED => {
    "changed": true,
    "checksum": "3b27d49ab53f170b384d5608fb9e3738f69d44fa",
    "dest": "/tmp/foo",
    "gid": 0,
    "group": "root",
    "md5sum": "349210dadef1c2366d62b0c4314c2c5b",
    "mode": "0644",
    "owner": "root",
    "size": 305,
    "src": "/root/.ansible/tmp/ansible-tmp-1590317158.994046-72047-272241656453308/source",
    "state": "file",
    "uid": 0
}
deb1 | CHANGED => {
    "changed": true,
    "checksum": "3b27d49ab53f170b384d5608fb9e3738f69d44fa",
    "dest": "/tmp/foo",
    "gid": 0,
    "group": "root",
    "md5sum": "349210dadef1c2366d62b0c4314c2c5b",
    "mode": "0644",
    "owner": "root",
    "size": 305,
    "src": "/root/.ansible/tmp/ansible-tmp-1590317159.002979-72045-90011462229862/source",
    "state": "file",
    "uid": 0
}

Можно убедиться, что файл скопировался:

> docker exec -it containers-deb1-1 cat /tmp/foo
---
debs:
  hosts:
    deb1:
      ansible_port: 2222
      ansible_host: localhost
      ansible_user: root
      ansible_python_interpreter: /usr/bin/python3.7
    deb2:
      ansible_port: 2223
      ansible_host: localhost
      ansible_user: root
      ansible_python_interpreter: /usr/bin/python3.7

Попробуем установить пакет:

> ansible deb1 -m apt -a "name=acme state=present"
deb1 | CHANGED => {
    "cache_update_time": 1590317476,
    "cache_updated": false,
    "changed": true,
    "stderr": "debconf: delaying package configuration, since apt-utils is not installed\n",
    "stderr_lines": [
        "debconf: delaying package configuration, since apt-utils is not installed"
    ],
    "stdout": "...",
    "stdout_lines": [
			 "..."
    ]
}

В документации модуля apt есть описание параметра state. Из описания понятно, что для удаления нужно изменить его на absent. Попробуем "удалить" пакет с хоста, на который мы его не устанавливали:

> ansible deb2 -m apt -a "name=acme state=absent"
deb2 | SUCCESS => {
    "changed": false
}

А теперь тоже самое для всей группы:

> ansible debs -m apt -a "name=acme state=absent"
deb2 | SUCCESS => {
    "changed": false
}
deb1 | CHANGED => {
    "changed": true,
    "stderr": "",
    "stderr_lines": [],
    "stdout": "...",
    "stdout_lines": [
        "..."
    ]
}

Мои первые команды на любом новом сервере обычно такие (ssh под root-ом в разных вариациях):

> apt-get update
> apt-get upgrade

Так это будет выглядеть в виде ad-hoc команды:

> ansible debs -m apt -a "update_cache=yes upgrade=yes"
deb1 | SUCCESS => {
    "changed": false,
    "msg": "...",
    "stderr": "",
    "stderr_lines": [],
    "stdout": "...",
    "stdout_lines": [
        "..."
    ]
}
deb2 | SUCCESS => {
    "changed": false,
    "msg": "...",
    "stderr": "",
    "stderr_lines": [],
    "stdout": "...",
    "stdout_lines": [
        "..."
    ]
}

Разовые команды - 2

Следующие часто выполняемые команды обычно про настройку пользователя и SSH доступа. Я эти команды не могу запомнить, поэтому обычно нахожу статью вроде Digital Ocean "Initial Server Setup with Debian 10" и следую шагам/командам, не читая саму статью. Получается так:

> adduser biozz
> usermod -aG sudo biozz

В наших контейнерах сейчас нет утилиты sudo, можно её установить:

> ansible debs -m apt -a "name=sudo state=present"

Когда создаёшь пользователя есть возможность интерактивно создать пароль. Модуль user в Ansible предлагает сделать это с помощью зашифрованной строки:

ansible debs -m user -a "name=biozz groups=sudo password={{ 'test' | password_hash('sha512') }}"

Остался ещё один пример - управление сервисами. Будем испытвать на nginx.

> ansible debs -m apt -a "name=nginx state=present"

Запустим сервис

> ansible debs -m service -a "name=nginx state=started"
deb1 | CHANGED => {
    "changed": true,
    "name": "nginx",
    "state": "started"
}
deb2 | CHANGED => {
    "changed": true,
    "name": "nginx",
    "state": "started"
}

Повторное выполнение той же команды, просто чтобы посмотреть на вывод:

> ansible ansible debs -m service -a "name=nginx state=started"
deb2 | SUCCESS => {
    "changed": false,
    "name": "nginx",
    "state": "started"
}
deb1 | SUCCESS => {
    "changed": false,
    "name": "nginx",
    "state": "started"
}

В статье ещё предлагает посмотреть на вывод модуля setup, но я считаю, что на этом этапе это не нужно. Там слишком много информации. Чисто ради интереса можно сделать:

> ansible all -m setup

Плейбуки

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

Синтаксис

Плейбуки пишут на YAML. В мануале предлагается установить и использовать утилиты для валидации, типа ansible-lint, но для начала хватит простой подстветки. Отличный источник для вдохновения и примеров - ansible-examples.

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

Если взять часть команд из 2-ой статьи и собрать их, то получится:

---
- hosts: debs
  tasks:
    - name: check terminal setup
      shell:
        cmd: echo $TERM
    - name: install acme package
      apt:
        name: acme
        state: present
        update_cache: true
    - name: install sudo package
      apt:
        name: sudo
        state: present
    - name: setup biozz user
      user:
        name: biozz
        groups: sudo
        password: "{{ 'test' | password_hash('sha512') }}"

Сохраним это в playbook.yml и запустим (убрал повторяющийся вывод):

ansible-playbook playbook.yml
PLAY [debs] ********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [deb1]
ok: [deb2]

TASK [xxx] ****************************************************
changed: [deb1]
changed: [deb2]

PLAY RECAP *********************************************************************
deb1                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
deb2                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

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

ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)

Syntax Error while loading YAML.
  found character that cannot start any token

The error appears to be in '/Users/biozz/other/ansible/playbook.yml': line 20, column 29, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

        groups: sudo
        password: {{ 'test' | password_hash('sha512') }}
                            ^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:

    with_items:
      - {{ foo }}

Should be written as:

    with_items:
      - "{{ foo }}"

Мы уже довольно много команд выполнили под root-ом, что считается небезопасным подходом. А у нас уже есть пользователь, поэтому можно переключиться на него для выполнения команд:

---
- hosts: debs
	remote_user: biozz
	become: yes
	become_method: sudo

https://docs.ansible.com/ansible/latest/user_guide/playbooks.html#working-with-playbooks

https://github.com/ansible/ansible-examples/tree/master/

https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html#handlers-running-operations-on-change

Материалы из этой части

Разные конфиги для разных хостов

TL;DR: в названии файлов конфигов можно указывать inventory_hostname_short, например telegraf.myhost.conf, _short чтобы можно было в инвентаре делать myhost.this и myhost.that, так как это удобно для разделения ssh-конфигов

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

Нас интересует полностью автоматический метод, поэтому остановимся на переменных инвентаря. Примеры написаны для Telegraf, этакий комбайн для сбора, обработки и отправки метрик.

Полностью рабочие примеры проектов можно найти тут: src/00_sources/06_tricks/01_host_specific_configs. Запускается из папки с инвентарём с помощью ansible-playbook main.yml.

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

Вот список файлов для этого подхода:

├── inventory
├── main.yml
└── roles
    └── telegraf
        ├── defaults
        │   └── main.yml
        ├── tasks
        │   └── main.yml
        └── templates
            └── telegraf.conf.j2

Сначала рассмотрим инвентарь:

deb1 is_net_enabled=true
deb2

is_net_enabled=true - это тот самый флаг, который поможет внутри шаблона конфига. Условно, будем считать что с одного хоста мы хотим собирать метрики сети (net), а с другого нет.

Примечание: переменные инвентаря в таком виде будут в первой половине иерархии переменных и велика вероятность, что они перезапишутся из других мест, если указать где-то еще. Будьте внимательны.

И вот он в telegraf.conf.j2, шаблоне, который рендерится в tasks/main.yml:

{{ if is_net_enabled }}
# Read metrics about network interface usage
[[inputs.net]]
  # no configuration
{{ endif }}

Проблема в том, что со временем конфиги могут сильно отличаться и if-чики в коде начнут сбивать с толку.

inventory_hostname_short

То есть нам по сути нужно как-то завязаться на переменную названия хоста.

Для этого сценария дерево файлов изменится лишь в templates/:

└── templates
    ├── telegraf.deb1.conf.j2
    └── telegraf.deb2.conf.j2

Возможно появился вопрос, почему именно _short, а не обычный inventory_hostname? Это небольшой хак, который позволяет указывать один и тот же хост, только с разными суффиксами. Например, это полезно, когда подключаешься к домашнему серваку дома через локальный IP, а когда на выезде - через какой-нибудь VPN. Так вот, _short, как уже догадались, возьмёт часть до точки.

deb1.local ; -> deb1
deb1.vpn   ; -> deb1
deb2       ; -> deb2

Интересный факт, в разделе Special variables в официальной доке Ansible про это не написано. Зато наглядно показано в этой статье.

И тогда в tasks/main.yml можно написать так:

- name: Create telegraf configuration from template
  template:
    src: "templates/telegraf.{{ inventory_hostname_short }}.conf.j2"
    dest: "{{ telegraf_directory }}/telegraf.conf"

На выходе получилось два независимых конфига на двух разных хостах.

Это не супер-масштабируемо, так как при добавлении нового хоста для него нужно будет добавлять ещё один конфиг. Зато явно и наглядно.

Переиспользование плейбука

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

Суть этого трюка в двух моментах:

  • вынести общие таски деплоя в отдельный файл, чтобы импортировать
  • кастомизировать отдельные таски деплоя в переменных роли

Вот древо файлов для этого трюка:

├── app
│   ├── defaults
│   │   └── main.yml
│   └── tasks
│       └── main.yml
└── common
    └── app.yml

Как обычно, исходники можно найти тут - src/00_sources/06_tricks/02_playbook_reuse.

Предположим есть такой плейбук:

# 01_example/common/app.yml
---
- name: Create the domain in Cloudflare
  community.general.cloudflare_dns:
    zone: "{{ base_domain }}"
    record: "{{ app_name }}"
    type: A
    value: "{{ server_ipv4 }}"
    account_email: "{{ cloudflare_email }}"
    account_api_key: "{{ cloudflare_api_key }}"
  when: app_init

- name: Create directories
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
  with_items:
    - "{{ app_directory }}"
    - "{{ app_source_directory }}"
  when: app_init

- name: Download files via git
  ansible.builtin.git:
    repo: "{{ app_repository }}"
    dest: "{{ app_source_directory }}"
    force: true
    accept_hostkey: true
    key_file: "{{ git_ssh_key_path }}"

- name: Build docker image
  community.general.docker_image:
    build:
      path: "{{ app_source_directory }}"
    name: "{{ app_name }}"
    tag: latest
    source: build
    state: present

- name: Start docker container
  community.docker.docker_container:
    name: "{{ app_name }}"
    image: "{{ app_name }}:latest"
    state: started
    pull: false
    recreate: true
    restart_policy: "unless-stopped"
    command: "{{ app_command }}"
    labels: "{{ app_labels | items2dict }}"

- name: Wait for the app to be online
  uri:
    url: "https://{{ app_host }}"
    status_code: 200
  register: result
  until: result.status == 200
  retries: 60
  delay: 1

- name: "Done"
  debug:
    msg: "https://{{ app_host }}"

Который можно кастомизировать с помощью переменных вот так:

---
app_init: no
app_directory: "/path/to/myapp"
app_source_directory: "{{ app_directory }}/src"
app_repository: "ssh://[email protected]/myapp.git"
app_name: "myapp"
app_domain: "{{ app_name }}"
app_host: "{{ app_domain }}.{{ base_domain }}"
app_internal_port: "8080"

app_command:
  - "--db-uri=mongodb://mongodb:27017"
  - "server"
  - "--bind=:{{ app_internal_port }}"

app_labels:
  - key: "traefik.enable"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.entrypoints"
    value: "https"
  - key: "traefik.http.routers.{{ app_name }}.rule"
    value: "Host(`{{ app_host }}`)"
  - key: "traefik.http.routers.{{ app_name }}.tls"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.tls.certresolver"
    value: "letsencrypt"
  - key: "traefik.http.services.{{ app_name }}.loadbalancer.server.port"
    value: "{{ app_internal_port }}"

Из интересных вещей тут следующие:

  • команды для запуска внутри контейнера в одной переменной в виде списка
app_command:
  - "--db-uri=mongodb://mongodb:27017"
  - "server"
  - "--bind=:{{ app_internal_port }}"
  • лейблы для контейнера разложены на key/value, которые дальше в плейбуке раскладываются с помощью items2dict
# 01_example/app/defaults/main.yml
app_labels:
  - key: "traefik.enable"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.entrypoints"
    value: "https"
  - key: "traefik.http.routers.{{ app_name }}.rule"
    value: "Host(`{{ app_host }}`)"
  - key: "traefik.http.routers.{{ app_name }}.tls"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.tls.certresolver"
    value: "letsencrypt"
  - key: "traefik.http.services.{{ app_name }}.loadbalancer.server.port"
    value: "{{ app_internal_port }}"
# 01_example/common/app.yml
    labels: "{{ app_labels | items2dict }}"

И теперь всё, что остаётся для каждого нового приложения - это заимпортировать из common/app.yml и изменить переменные по вкусу:

# 01_example/app/tasks/main.yml
---
- name: Generic app tasks
  ansible.builtin.include_tasks: "../../common/tasks/app.yml"

Использование Taskfile

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

Я фанат Taskfile. Я даже написал плагин для Sublime Text. Я использую Taskfile практическо во всех своих проектах. И Ansible-скрипты не исключение.

Вот небольшой кусочек Taskfile в моем проекте с Ansible-скриптами:

version: '3'

tasks:
  requirements:
    cmds:
      - ansible-galaxy install -r requirements.yml --force
    silent: true
  run:
    cmds:
      - ansible-playbook main.yml -i deb1, --tags {{ .CLI_ARGS }}
  run-deb2:
    cmds:
      - ansible-playbook main.yml -i deb2, --tags {{ .CLI_ARGS }}

Хочется остановиться на run и run-deb2.

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

Используются эти команды вот так: task run -- tag1, где tag1 - это тег роли или нескольких ролей. Подробнее про теги можно почитать в следующей галаве.

Теги

В трюке про Taskfile была упомянута команда task run -- tag1. В этой главе я покажу, что нужно, чтобы эта команда заработала.

На самом деле всё довольно просто. Рассмотрим пример проекта:

.
├── inventory
├── main.yml
└── roles
    ├── myrole1
    │   └── tasks
    │       └── main.yml
    ├── myrole2
    │   └── tasks
    │       └── main.yml
    └── myrole3
        └── tasks
            └── main.yml

В главном плейбуке main.yml перечисляются роли и к каждой роли подписывается набор тегов:

---
- name: Deploy
  hosts: all
  become: true
  roles:
    - { role: myrole1, tags: [foo, init] }
    - { role: myrole2, tags: [bar, init] }
    - { role: myrole3, tags: [buz] }

Я использую теги двумя способами и в этом суть этого трюка:

  • когда нужно запустить конкретную роль task run -- foo или task run -- bar
  • когда нужно запустить серию плейбуков на какой-то группе хостов tas run -- init