Стадия жизненного цикла модуля: Experimental
У модуля есть требования для установки

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

Ресурсы модуля

Модуль добавляет в кластер два кастомных ресурса, приведённых в таблице ниже.

Ресурс Назначение
AnsibleRun Задание, которое выполняет плейбук один раз. Задание определяет целевые хосты, выполняет на них плейбук и записывает результат в свой статус. Повторно задание не выполняется, а его блок spec изменять нельзя, поэтому для нового выполнения создаётся новый объект
AnsibleRunSchedule Расписание в формате cron, которое создаёт объекты AnsibleRun так же, как ресурс CronJob создаёт объекты Job

Далее в тексте под заданием понимается объект AnsibleRun, а под плейбуком — YAML-файл с задачами, который это задание выполняет.

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

Быстрый старт

Пример настройки виртуальной машины, в котором задание проверяет связь с машиной и записывает результат в свой статус.

  1. Подготовьте виртуальную машину. Для подключения потребуются SSH-сервер, пользователь для входа и адрес, известный платформе. Ниже приведён фрагмент cloud-init-сценария, который создаёт такого пользователя:

    #cloud-config
    packages:
      - openssh-server
    users:
      - name: ansible
        lock_passwd: false
        sudo: ALL=(ALL) NOPASSWD:ALL
        ssh_authorized_keys:
          - ssh-ed25519 <SSH_PUBLIC_KEY> ansible@example
    runcmd:
      - systemctl enable --now ssh

    В параметре <SSH_PUBLIC_KEY> укажите публичный ключ, приватная часть которого будет помещена в Secret на следующем шаге.

  2. Создайте Secret с учётными данными SSH в том же неймспейсе, где будет создано задание:

    apiVersion: v1
    kind: Secret
    metadata:
      name: ssh-creds
      namespace: demo
    type: Opaque
    stringData:
      username: ansible
      ssh-privatekey: |
        -----BEGIN OPENSSH PRIVATE KEY-----
        ...
        -----END OPENSSH PRIVATE KEY-----
  3. Создайте задание, которое проверит связь с машиной:

    apiVersion: ansible.deckhouse.io/v1alpha1
    kind: AnsibleRun
    metadata:
      name: configure-vm
      namespace: demo
    spec:
      target:
        type: VirtualMachines
        virtualMachines:
          selector:
            matchLabels:
              vm: vm-01
      connection:
        secretRef:
          name: ssh-creds
      playbook:
        type: Inline
        inline: |
          ---
          - name: Configure VM
            hosts: all
            tasks:
              - name: Check SSH connectivity
                ansible.builtin.ping:
  4. Проверьте результат выполнения задания:

    d8 k get ansibleruns -n demo

    Пример вывода:

    NAME           PHASE               REASON              HOSTS   OK   FAILED   UNREACHABLE   SKIPPED   AGE
    configure-vm   PlaybookSucceeded   PlaybookSucceeded   1       1    0        0             0         42s

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

    d8 k get ansiblerun configure-vm -n demo -o yaml

Структура задания

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

Блок Назначение Обязателен
target Определяет, какие хосты настраивает задание Да
connection Определяет, как задание подключается к хостам Да
playbook Определяет источник плейбука, теги выполняемой части и переменные Да
runner Определяет режим выполнения плейбука Нет

Ниже приведён манифест, в котором заполнены все блоки задания:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: configure-db
  namespace: demo
spec:
  target:
    type: VirtualMachines            # VirtualMachines | Hosts
    virtualMachines:
      selector:                      # selector ИЛИ names, но не оба
        matchLabels:
          role: db
  connection:
    secretRef:
      name: ssh-creds                # учётные данные SSH, тот же namespace
    network:                         # необязательно; по умолчанию основная сеть
      type: ClusterNetwork           # Main | Network | ClusterNetwork
      name: vlan-64
  playbook:
    type: Inline                     # Inline | ConfigMap | Git
    inline: |
      ---
      - hosts: all
        tasks:
          - ansible.builtin.ping:
    tags: [deploy]                   # необязательно; выполнить только эти теги
    skipTags: [migrations]           # необязательно; исключить эти теги
    vars:                            # необязательно; --extra-vars этого задания
      - name: app_version
        value: "1.4.2"               # любой YAML: строка, число, список, отображение
      - name: db_password
        valueFrom:
          secretKeyRef:
            name: app-secrets
            key: db-password
    varsFiles:                       # необязательно; YAML-документ с переменными
      - configMapRef:
          name: app-config           # ключ по умолчанию — vars.yaml
  runner:
    dryRun: false                    # режим проверки Ansible
    diff: false
    verbosity: 0                     # 0..4

Цели

Блок target определяет хосты, на которых выполняется плейбук. Такими хостами являются либо виртуальные машины платформы, либо хосты, заданные адресом. Одно задание работает с хостами только одного вида, поэтому указать поля virtualMachines и hosts в одном объекте нельзя.

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

Виртуальные машины

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

Ниже приведён пример выбора машины по лейблу:

target:
  type: VirtualMachines
  virtualMachines:
    selector:
      matchLabels:
        vm: vm-01

Пример выбора машин по выражению над лейблами:

target:
  type: VirtualMachines
  virtualMachines:
    selector:
      matchExpressions:
        - key: vm
          operator: In
          values: [vm-01, vm-02]

Пример выбора машин по именам:

target:
  type: VirtualMachines
  virtualMachines:
    names: [demo-vm-01, demo-vm-02]

Поля selector и names являются взаимоисключающими, а манифест с пустым селектором кластер отклоняет. Задание выполняется однократно, поэтому цели требуется задавать явно.

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

Хосты, заданные адресом

Адреса некоторых хостов платформе неизвестны. К таким хостам относятся машины с адресом, настроенным вручную внутри гостевой ОС или полученным от внешнего DHCP-сервера, а также физические серверы, сетевые устройства и машины из других кластеров. Для настройки таких хостов используется тип целей Hosts, в котором адреса указываются в манифесте задания.

Цели типа Hosts доступны только в коммерческих редакциях. В Community Edition задание работает с виртуальными машинами платформы, а манифест с целями типа Hosts кластер отклоняет при создании.

Пример задания с хостами, заданными адресом:

target:
  type: Hosts
  hosts:
    - address: 192.168.55.10
      vars:
        - name: app_role
          value: frontend
      groups: [web]
    - address: db.example.com

Хост, заданный адресом, не является ресурсом VirtualMachine, поэтому задание работает с ним иначе, чем с виртуальной машиной платформы. Различия приведены в таблице ниже.

Свойство VirtualMachines Hosts
Адрес Определяет платформа Указывается в манифесте задания
Фаза цели Ожидается Running или Migrating Не применяется
Проверка занятости другим заданием Выполняется, причина пропуска TargetBusy Не выполняется
Переменные и группы Аннотации на машине Поля vars и groups
status.hosts[].name Имя машины Адрес хоста
status.skippedHosts Заполняется, если есть пропущенные машины Всегда пустой

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

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

Инвентарь Ansible

Файл инвентаря создавать не требуется. Контроллер формирует его самостоятельно на основе выбранных целей и аннотаций виртуальных машин. Переменные ansible_host, ansible_user и пути к учётным данным контроллер задаёт сам, и переопределить их нельзя. Остальные переменные задаются пользователем.

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

Аннотация Назначение
ansible.deckhouse.io/groups Список групп через запятую. Хост входит в каждую из перечисленных групп дополнительно к группе all
vars.ansible.deckhouse.io/<VARIABLE_NAME> Переменная хоста с именем <VARIABLE_NAME>
apiVersion: virtualization.deckhouse.io/v1alpha2
kind: VirtualMachine
metadata:
  name: demo-vm-01
  namespace: demo
  annotations:
    ansible.deckhouse.io/groups: "web,production"
    vars.ansible.deckhouse.io/app_role: "frontend"

Аннотации виртуальной машины выполняют ту же роль, что файл host_vars/<HOST_NAME> в обычном проекте Ansible, но хранятся на самой машине. Для хоста, заданного адресом, те же данные указываются в манифесте задания полями vars и groups.

В обоих случаях контроллер формирует файл инвентаря следующего вида:

all:
  hosts:
    demo-vm-01:
      ansible_host: 10.66.10.2
      ansible_user: ansible
      ansible_ssh_private_key_file: /home/runner/.ssh/ssh-privatekey
      app_role: frontend
web:
  hosts:
    demo-vm-01: {}
production:
  hosts:
    demo-vm-01: {}

К именам переменных в аннотациях применяются те же правила, что и к запрещённым именам переменных. Дополнительно действует правило, которое применяется только к аннотациям. Аннотация не может задавать переменные с префиксом ansible_, за исключением переменной ansible_port.

Это ограничение связано с тем, что аннотацию может изменить владелец виртуальной машины, который не обязательно является автором задания. Например, переменная ansible_host в аннотации перенаправила бы чужое задание на другую машину и передала бы ей учётные данные SSH, а переменная ansible_ssh_common_args со значением ProxyCommand выполнила бы произвольную команду внутри пода раннера.

Переменная ansible_port является исключением, потому что SSH-порт относится к конкретной машине. Поле spec.playbook.vars задаёт один порт для всего задания, а машины выбираются по лейблам, поэтому нестандартный порт известен только самой машине:

metadata:
  annotations:
    vars.ansible.deckhouse.io/ansible_port: "2222"

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

Таким образом, порт задаётся в трёх местах, от менее приоритетного к более приоритетному:

  • spec.playbook.vars задаёт порт для всех хостов задания.
  • hosts[].vars задаёт порт для отдельного хоста, заданного адресом.
  • Аннотация машины задаёт порт только для этой машины и переопределяет оба предыдущих значения.

Аннотации, имена которых не начинаются с двух перечисленных префиксов, контроллер игнорирует, а пустые элементы в списке групп пропускает. Если правило именования нарушено, задание завершается фазой Error с причиной InvalidInventoryAnnotations до создания пода. В сообщении статуса при этом указаны машина и аннотация, которые нарушили правило.

Подключение

Блок connection определяет, по какому транспорту, с какими учётными данными и через какую сеть задание подключается к хостам.

Транспорт

Транспорт определяет способ подключения задания к хосту и указывается в поле connection.type. В текущей версии модуля доступно единственное значение SSH, которое используется по умолчанию, поэтому поле можно не указывать:

connection:
  type: SSH
  secretRef:
    name: ssh-creds

Учётные данные SSH

Учётные данные для подключения задание берёт из Secret, имя которого указано в поле connection.secretRef:

connection:
  secretRef:
    name: ssh-creds

Такой Secret должен находиться в неймспейсе задания и содержать ключи, приведённые в таблице ниже. Обязательным является хотя бы один из ключей ssh-privatekey или password.

Ключ Назначение
username Пользователь SSH
ssh-privatekey Приватный ключ SSH
password Пароль SSH
become-password Пароль для повышения привилегий, который используется задачами с параметром become: true
ansible-vault-password Пароль для Ansible Vault. Передаётся утилите ansible-playbook как файл пароля

Имена ключей соответствуют встроенным типам Secret kubernetes.io/ssh-auth и kubernetes.io/basic-auth.

Ниже приведён пример Secret, в котором вместо приватного ключа используется пароль:

apiVersion: v1
kind: Secret
metadata:
  name: ssh-creds-password
  namespace: demo
type: Opaque
stringData:
  username: ansible
  password: ansible

Сети

По умолчанию задание выполняется в основной сети. Под раннера запускается в сети подов кластера и подключается к машине по адресу из поля status.ipAddress ресурса VirtualMachine.

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

Виртуальная машина может быть подключена также к дополнительным сетям модуля sdn — проектной (Network) или кластерной (ClusterNetwork). Служба SSH в гостевой ОС может слушать только в такой сети.

Дополнительная сеть является отдельным L2-доменом, и маршрута из сети подов в неё нет, поэтому одного адреса машины для подключения недостаточно. Задание должно выполняться в той же сети, в которой находится машина.

Сеть указывается в блоке connection.network. Под раннера подключается к указанной сети, и адрес каждой цели контроллер также берёт в ней:

connection:
  secretRef:
    name: ssh-creds
  network:
    type: ClusterNetwork   # Main (по умолчанию) | Network | ClusterNetwork
    name: vlan-64

Блок network повторяет элемент списка spec.networks виртуальной машины, поэтому его значения переносятся из манифеста машины:

# VirtualMachine
spec:
  networks:
    - type: Main
    - type: ClusterNetwork
      name: vlan-64        # эту сеть и указываем в задании
    - type: Network
      name: storage-net

Перед настройкой дополнительной сети учитывайте следующие особенности:

  • Пул адресов у сети (spec.ipam.ipAddressPoolRef) — адрес из него получают и машина, и раннер, поэтому сеть без пула работает только на уровне L2. Задание завершается фазой Error и причиной NetworkWithoutIPAM, а в сообщении указана сеть, которую нужно настроить. Под при этом не создаётся.
  • Настройка интерфейса в гостевой ОС — адрес доставляется по DHCP, поэтому в гостевой ОС нужен DHCP-клиент на этом интерфейсе. Иначе платформа показывает адрес, по которому машина не отвечает, и хост оказывается недостижимым (unreachable). Привязывайте интерфейс по MAC-адресу или по предсказуемому имени, а не по ethX, потому что подключение сети или изменение порядка в spec.networks может перенумеровать ethX внутри гостя, и тогда DHCP-клиент запросит адрес на другом интерфейсе.
  • Одна сеть на задание — машины, которые слушают в разных сетях, разделяются лейблами на несколько заданий. Из каждого манифеста видно, в какой сети оно выполняется.
  • Область видимости сети — Network видна только в своём неймспейсе, а ClusterNetwork доступна из любого.
  • Размер пула — адресов должно хватать на машины плюс одновременные задания, потому что задание занимает адрес, пока существует его под. Под успешного задания удаляется сразу, а под неуспешного сохраняется до удаления AnsibleRun, поэтому оставленные неуспешные задания продолжают занимать адреса.
  • Хосты, заданные адресом — им network тоже нужна, потому что сам адрес раннер в эту сеть не помещает. Так в сети управления настраивают то, чего в кластере нет, например физические серверы, сетевые устройства или машины других кластеров.

Под раннера остаётся подключённым и к сети подов кластера, потому что дополнительный интерфейс добавляется к основному, а не заменяет его. Поэтому плейбук из Git-репозитория внутри кластера и содержимое Galaxy из зеркала в кластере загружаются так же, как при выполнении задания без дополнительной сети. Init-контейнерам при этом по-прежнему доступны кластерный DNS и сервисы кластера.

Исключением является пул адресов, который выдаёт маршруты. Такие маршруты прописываются внутри пода, и маршрут по умолчанию (0.0.0.0/0) перенаправляет через дополнительную сеть весь исходящий трафик. Обращения к внутрикластерным адресам продолжат работать, поскольку для них есть отдельные маршруты. Данные, которые задание загружает извне кластера, например из публичного Git-репозитория или Galaxy, будут отправлены через дополнительную сеть и могут оказаться недоступны. Перед выполнением задания в такой сети проверьте список маршрутов в поле spec.pools[].routes пула.

Адрес появляется в списке status.networks[] виртуальной машины в момент, когда интерфейс фактически подключён к её поду, а не в момент выделения адреса. Ресурс IPAddress у машины при этом может уже существовать, пока статус ещё пуст. До подключения интерфейса задание пропускает такую машину с причиной NoAddressInNetwork.

Ещё три особенности дополнительных сетей следуют из их устройства, и администратору кластера важно учитывать их при выдаче прав на создание заданий:

  • Сетевые политики — NetworkPolicy проекта ограничивает только сеть подов и не ограничивает действия задания внутри VLAN.
  • Доступность ClusterNetwork — она видна из любого неймспейса, поэтому право создавать AnsibleRun равно праву поместить под в любой кластерный L2-домен. Так работает модуль sdn для любой нагрузки, и модуль ansible здесь ничего не добавляет. Но учитывать это следует до того, как выдать такое право.
  • MTU — берётся из сети и не должен превышать MTU узловых интерфейсов, на которых она построена. Несовпадение проявляется неочевидно. SSH подключается, а копирование файла или установка пакета зависают.

Плейбук

Блок playbook определяет, откуда взять плейбук и какую его часть выполнить. Источником плейбука может быть текст в манифесте задания, ресурс ConfigMap или Git-репозиторий. В одном задании используется только один источник.

Встроенный плейбук

Текст плейбука указывается непосредственно в манифесте задания:

playbook:
  type: Inline
  inline: |
    ---
    - name: Configure VM
      hosts: all
      tasks:
        - ansible.builtin.ping:

Размер встроенного плейбука ограничен 64 КБ. Для плейбука большего размера используйте ресурс ConfigMap или Git-репозиторий.

Плейбук из ConfigMap

Плейбук хранится в ресурсе ConfigMap в неймспейсе задания, а задание ссылается на ключ этого ресурса:

playbook:
  type: ConfigMap
  configMapRef:
    name: demo-playbook
    key: playbook.yaml     # значение по умолчанию

Плейбук-проект из Git

Для проектов с ролями, каталогом group_vars и шаблонами контроллер загружает всё дерево репозитория, поэтому роли, расположенные рядом с плейбуком, выполняются без изменений:

playbook:
  type: Git
  git:
    url: https://github.com/example-org/infra
    revision: v1.4.0             # ветка, тег или SHA; по умолчанию ветка репозитория
    path: playbooks/site.yml     # по умолчанию playbook.yaml
    secretRef:
      name: git-token            # приватные репозитории

Указанным настройкам соответствуют следующие команды Ansible:

git clone --recurse-submodules https://github.com/example-org/infra && cd infra
git checkout v1.4.0
ansible-galaxy install -r requirements.yml     # если проект их объявляет
ansible-playbook -i inventory.yml playbooks/site.yml

Если в загруженном дереве присутствует файл requirements.yml, roles/requirements.yml или collections/requirements.yml, объявленные в нём роли и коллекции Galaxy устанавливаются до запуска плейбука. Подмодули репозитория также загружаются. Коммит, который был фактически выполнен, контроллер записывает в поле status.playbookCommit.

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

Приватные репозитории

Для доступа к приватному репозиторию требуются отдельные учётные данные, не связанные с учётными данными SSH для целевых хостов. Такие данные контроллер берёт из Secret, указанного в поле git.secretRef, и монтирует только на шаг загрузки репозитория, поэтому задачи плейбука их не видят.

Для адресов вида ssh://git@… Secret является обязательным и должен содержать ключи ssh-privatekey и known_hosts, поскольку ключи хоста проверяются строго. Получить содержимое known_hosts можно командой ssh-keyscan github.com.

apiVersion: v1
kind: Secret
metadata:
  name: git-deploy-key
  namespace: demo
type: kubernetes.io/ssh-auth
stringData:
  ssh-privatekey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...
  known_hosts: |
    github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...

Для адресов HTTP(S) с токеном требуются ключи username и password, где паролем является сам токен. В GitLab в качестве имени пользователя используется значение oauth2. Если репозиторий размещён на собственном сервере с сертификатом своего удостоверяющего центра, добавьте в Secret ключ ca.crt:

apiVersion: v1
kind: Secret
metadata:
  name: git-token
  namespace: demo
type: kubernetes.io/basic-auth
stringData:
  username: oauth2
  password: glpat-xxxxxxxxxxxx

Выборочный запуск по тегам

Теги определяют, какая часть плейбука выполняется, и работают так же, как параметры --tags и --skip-tags командной строки Ansible.

playbook:
  type: Git
  git:
    url: https://github.com/example-org/infra
    path: site.yml
  tags: [deploy, config]     # выполнить только помеченные этими тегами задачи
  skipTags: [migrations]     # и исключить эти

Приведённым настройкам соответствует следующая команда:

ansible-playbook -i inventory.yml site.yml --tags deploy,config --skip-tags migrations

Если заданы оба поля, выполняются задачи, выбранные полем tags, за вычетом задач, исключённых полем skipTags. Так же ведёт себя утилита ansible-playbook.

Именем тега может быть любая строка без пробелов и запятой. Запятая используется как разделитель тегов в аргументе, а пробелы Ansible удаляет самостоятельно. Остальные написания, принятые в плейбуках, допускаются, включая role:install и db/migrate.

Служебные теги Ansible сохраняют своё стандартное значение, которое приведено в таблице ниже.

Тег Значение
always Выполняется при любом фильтре, пока не назван в skipTags
never Выполняется, только когда назван в tags
tagged Все задачи, у которых есть хоть один тег
untagged Все задачи без тегов
all Все задачи — поведение по умолчанию

Фильтр без совпадений

Такая ситуация не является ошибкой. Утилита ansible-playbook завершается успешно, не выполнив ни одной задачи, а задание переходит в фазу PlaybookSucceeded с нулевыми счётчиками. Так же выглядит результат при опечатке в имени тега, поэтому задание сообщает об этом в статусе:

$ d8 k get ansiblerun configure-db -o jsonpath='{.status.message}'
No task matched the tags of the run.

Теги не связаны с параметрами раннера. Параметры dryRun и verbosity изменяют режим выполнения и подробность вывода, но не влияют на состав выполняемых задач.

Параметры раннера

Блок runner изменяет режим запуска плейбука и подробность его вывода, не влияя на состав выполняемых задач:

runner:
  dryRun: true      # режим проверки Ansible
  diff: true
  verbosity: 2      # 0..4

Приведённым настройкам соответствует следующая команда:

ansible-playbook -i inventory.yml site.yml --check --diff -vv

С параметром dryRun задание показывает, какие изменения были бы выполнены, а вместе с параметром diff выводит эти изменения подробно. В режиме проверки Ansible пропускает задачи модулей command и shell.

Если Secret подключения содержит ключ ansible-vault-password, контроллер добавляет к запуску параметр --vault-password-file, поэтому проект с зашифрованными файлами выполняется без изменений.

Переменные

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

В простейшем случае переменная содержит значение, которое плейбук читает как {{ app_version }}:

spec:
  playbook:
    vars:
      - name: app_version
        value: "1.4.2"

Формат записи совпадает с форматом блока env пода. У переменной есть имя, а её значение либо указывается непосредственно в поле value, либо берётся из другого объекта через поле valueFrom. В поле value можно указать строку, число, список или отображение.

Значение переменной задаётся тремя способами, которые приведены в таблице ниже.

Способ Назначение
vars с полем value Значение принадлежит этому заданию и указывается непосредственно в манифесте
vars с полем valueFrom Значение уже хранится в ключе Secret или ConfigMap
varsFiles Требуется передать целый набор значений со структурой и типами

Ниже приведён манифест, в котором использованы все три способа:

spec:
  playbook:
    vars:
      - name: app_version
        value: "1.4.2"
      - name: packages
        value: [nginx, curl, git]
      - name: users
        value:
          - name: deploy
            groups: [wheel]
      - name: db_password
        valueFrom:
          secretKeyRef:
            name: app-secrets
            key: db-password
    varsFiles:
      - configMapRef:
          name: app-config

Приведённым настройкам соответствует следующая команда:

ansible-playbook -i inventory.yml site.yml \
  -e @app-config-vars.yml \
  -e '{"app_version":"1.4.2","packages":["nginx","curl","git"],"users":[{"name":"deploy","groups":["wheel"]}]}' \
  -e db_password="$DB_PASSWORD"

Поле spec.playbook.vars соответствует полю Extra Variables в AWX, параметру -e командной строки и имеет такой же высший приоритет.

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

Значения из Secret и ConfigMap

Пароль или другое уже существующее значение не требуется переносить в манифест задания. Поле valueFrom берёт значение из одного ключа Secret или ConfigMap, при этом плейбук видит переменную под тем именем, которое указано в задании:

spec:
  playbook:
    vars:
      - name: db_password
        valueFrom:
          secretKeyRef:
            name: app-secrets
            key: db-password

Значение подставляет kubelet при запуске пода раннера. Контроллер обращается к Secret и ConfigMap только для проверки имён ключей, поэтому значение не попадает ни в один объект модуля и не появляется в аргументах процесса, которые видны в выводе ps. В файле переменных остаётся только ссылка на переменную окружения:

db_password: "{{ lookup('env', 'ANSIBLE_VAR_db_password') }}"

Из такого способа передачи значений следуют перечисленные ниже особенности.

  • Права на Secret — при выполнении они не проверяются, так же как для любого пода, который на Secret ссылается. Значит, права создавать задание достаточно, чтобы прочитать содержимое названного в задании объекта, ведь плейбук пишет владелец задания. Учитывайте это, выдавая такое право в неймспейсе, где содержимое Secret доступно не всем.
  • Имя переменной окруженияANSIBLE_VAR_ плюс имя переменной как написано, с сохранением регистра. Ansible считает db_password и DB_PASSWORD разными переменными, и в окружении это две разные записи. Поэтому Secret с ключами в UPPER_SNAKE соседствует с переменной, названной по конвенции Ansible, без переименований.
  • Одно имя, одно значение — если одно имя получает значения из двух разных ключей (например, два хоста называют каждый свой Secret), задание отклоняется с причиной InvalidVariables, потому что окружение у пода раннера одно.
  • Ключ с двоичными данными (binaryData у ConfigMap) — переменной стать не может. Если он указан явно, задание завершается ошибкой с соответствующим сообщением.

Тип значения из отдельного ключа

В ключе Secret или ConfigMap хранится строка, и именно она передаётся плейбуку. Поле value и документ, указанный в varsFiles, сохраняют форму значения, а отдельный ключ — нет.

vars:
  - name: packages
    value: [nginx, curl]          # список передаётся списком
  - name: extra_packages
    valueFrom:
      configMapKeyRef:            # "[nginx, curl]" — строка
        name: app-config
        key: extra_packages

Если плейбуку требуется список, он разбирает строку самостоятельно:

- hosts: all
  tasks:
    - ansible.builtin.package:
        name: "{{ extra_packages | from_yaml }}"
        state: present

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

Набор переменных в файле

Набор переменных хранится в файле так же, как в файле group_vars/all.yml обычного проекта Ansible. Разместите такой файл в ресурсе ConfigMap или Secret и укажите его в задании. Этот способ соответствует параметру -e @vars.yml командной строки.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  vars.yaml: |                  # обычный YAML-документ
    app_tier: backend
    app_version: "1.10"         # в кавычках — значит строка
    packages: [nginx, curl]     # список остаётся списком
    limits:
      cpu: 2
      mem: 4Gi
spec:
  playbook:
    varsFiles:
      - configMapRef:
          name: app-config        # ключ по умолчанию — vars.yaml
      - secretRef:
          name: app-secrets
          key: prod.yaml
          optional: true

Ключом по умолчанию является vars.yaml, так же как ключом по умолчанию для ConfigMap с плейбуком является playbook.yaml. Указывайте поле key, если в одном объекте хранится несколько наборов переменных, например dev.yaml и prod.yaml.

Основное назначение файла состоит в том, что типы значений в нём сохраняются. Автор файла указывает, какое значение является строкой, а какое числом, тогда как отдельный ключ Secret или ConfigMap такой возможности не даёт. Например, значение 1.10 в отдельном ключе является текстом, и его преобразование в число превратило бы версию в 1.1.

Файл читает утилита ansible-playbook, а не контроллер, поскольку kubelet монтирует ключ в под раннера. Из этого следуют два вывода:

  • Значения не проходят ни через один объект модуля и не появляются в аргументах процесса. Их не видно ни в манифесте задания, ни в списке процессов на узле.
  • Имена переменных внутри файла не проверяются — имена, которые запрещены в других местах, из файла действуют. Отдельно следует отметить переменную ansible_connection: local, при которой плейбук выполнится внутри пода раннера и завершится успешно, не затронув ни одного целевого хоста.

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

Переменные хоста

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

Поле hosts[].vars описывается так же, как переменные задания, включая источники значений valueFrom:

target:
  type: Hosts
  hosts:
    - address: 192.168.55.10
      vars:
        - name: app_role
          value: frontend
        - name: ansible_port
          value: 2222
      groups: [web]

Такие переменные попадают в файл инвентаря рядом с машиной, которую описывают, и предназначены для фактов о ней, например для значений datacenter, app_role или нестандартного SSH-порта. Для виртуальной машины те же значения задают аннотации.

Переменная с тем же именем из поля spec.playbook.vars имеет приоритет над обоими способами, поскольку аннотация задана заранее, а значения задания применяются в момент выполнения.

Исключением является случай, когда обе стороны получают значение из Secret или ConfigMap и при этом из разных ключей. В такой ситуации приоритет не применяется, и задание отклоняется с причиной InvalidVariables до запуска плейбука, поскольку окружение у пода раннера одно. Чтобы этого избежать, задайте таким значениям разные имена. На два литеральных значения или на литеральное значение вместе с valueFrom ограничение не распространяется.

Приоритет значений

Для значений действует общее правило: переменные задания имеют приоритет над всеми значениями, которые задают плейбук и его проект. Переменная хоста, заданная аннотацией виртуальной машины или полем hosts[].vars, имеет приоритет над каталогом group_vars/ проекта, но уступает блоку vars внутри play.

Все значения из полей vars и varsFiles передаются как --extra-vars, то есть на 22-м, самом высоком уровне приоритета Ansible. Полный порядок приоритетов, от низшего к высшему, приведён в таблице ниже.

Откуда значение Уровень Ansible
defaults роли 2
group_vars/all.yml проекта 5
Переменные хоста: аннотации виртуальной машины, hosts[].vars 8
vars: в play 12
vars_files 14
Переменные роли 15
set_fact 19
spec.playbook.varsFiles, в порядке списка 22
spec.playbook.vars 22

Play в таблице означает блок вида - hosts: … tasks: …, и в одном плейбуке таких блоков может быть несколько.

Поля spec.playbook.vars и spec.playbook.varsFiles находятся на одном уровне приоритета, а между собой их различает порядок аргументов. Сначала передаются файлы в порядке списка, а поле spec.playbook.vars передаётся последним. Утилита ansible-playbook применяет параметры -e слева направо, поэтому итоговый порядок виден в аргументах пода, где значение правее имеет приоритет.

d8 k -n <NAMESPACE> get pod -l ansible.deckhouse.io/owner=<RUN_NAME> \
  -o jsonpath='{.spec.containers[0].args}'
# … -e @/ansible/vars.d/0/vars.yaml -e @/ansible/vars.d/1/prod.yaml -e @/ansible/vars.yml

Значение с более высоким приоритетом заменяет имя переменной целиком, и отображения не объединяются. Например, значение limits: {cpu: 2, mem: 4Gi} в одном файле и значение limits: {cpu: 4} в следующем дадут плейбуку limits: {cpu: 4}, а значение mem будет потеряно. Поэтому храните одно отображение в одном файле, а отдельное имя переопределяйте через поле spec.playbook.vars, значение которого нельзя перекрыть ни одним файлом.

Из описанного порядка приоритетов следует, что общий Git-проект не может защитить свои имена переменных от задания. Значения, принадлежащие проекту, храните в его каталоге group_vars/, а поле vars используйте для значений, которые различаются между заданиями.

Разрешение конфликта имён

В примере ниже плейбук задаёт переменную app_version самостоятельно, на машине задана аннотация с тем же именем, а задание передаёт собственное значение:

# VirtualMachine
metadata:
  annotations:
    vars.ansible.deckhouse.io/app_version: "from-annotation"   # уровень 8
---
# AnsibleRun
spec:
  playbook:
    vars:
      - name: app_version
        value: "from-the-run"                                    # уровень 22
    playbook:
      type: Inline
      inline: |
        - hosts: all
          vars:
            app_version: "from-the-playbook"                     # уровень 12
          tasks:
            - ansible.builtin.debug:
                msg: "{{ app_version }}"

Задача выведет значение from-the-run. Если удалить поле spec.playbook.vars, задача выведет значение from-the-playbook, а не значение аннотации, поскольку уровень 12 выше уровня 8. Значение аннотации применяется только в том случае, если плейбук эту переменную не задаёт.

Типы значений

Тип значения определяется тем, как это значение записано в манифесте. Схема ресурса проверяет только имена переменных, а содержимое значений не проверяется, поэтому опечатка внутри вложенной структуры обнаружится при выполнении плейбука.

Здесь также действует известная особенность YAML, в котором значение 1.10 является числом 1.1, а значение no — логическим false. Версия является строкой, поэтому её следует указывать в кавычках:

vars:
  - name: app_version
    value: "1.10"      # без кавычек превратится в 1.1

Целые числа сохраняют свой тип, поэтому значение value: 1000000 передаётся плейбуку целым числом 1 000 000, а большое целое число не теряет точность.

Значение с фигурными скобками

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

Как записано Что получит плейбук
value: "{{ 7 * 6 }}" в манифесте 42
Тот же текст в Secret или ConfigMap {{ 7 * 6 }}, без вычисления

Поле value задаёт автор задания, поэтому его значение шаблонизируется так же, как переменная в любом файле переменных. Значение из поля valueFrom передаётся через выражение lookup('env', ...), и его результат Ansible помечает как небезопасный (unsafe). Благодаря этому содержимое чужого объекта не может стать выражением, выполняемым внутри пода раннера.

Чтобы фигурные скобки в поле value остались литеральными, экранируйте их так, как этого ожидает Ansible:

vars:
  - name: template_source
    value: "{{ '{{' }} not a template {{ '}}' }}"   # приедет как {{ not a template }}

Отсутствующий источник переменных

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

Ситуация Поведение задания
Объекта нет, optional не задан Задание остаётся в Pending с DependenciesReady=False и причиной VarsSourceNotFound, а когда объект появится — продолжается. Виртуальные машины при этом не занимаются
Объекта нет, optional: true Задание выполняется без этих переменных
Объект есть, но нет ключа, названного в valueFrom Задание завершается ошибкой DependencyInvalid
Объект есть, ключа нет, у селектора optional: true Переменная отсутствует

Для несуществующего ключа ссылка на переменную не формируется, поэтому в плейбуке продолжают работать конструкции when: x is defined и фильтр default().

Поле optional есть у источников переменных и отсутствует у полей connection.secretRef и playbook.configMapRef, поскольку задание без учётных данных и без плейбука выполнить нельзя.

Запрещённые имена переменных

Именем переменной является идентификатор Ansible, соответствующий шаблону [A-Za-z_][A-Za-z0-9_]*. Например, выражение {{ app-role }} Ansible трактует не как обращение к переменной, а как вычитание. Имя, не соответствующее шаблону, отклоняет API-сервер, поэтому ошибка возвращается в ответ на команду d8 k apply и содержит имя поля:

The AnsibleRun "pattern-check" is invalid:
* spec.target.hosts[0].vars[0].name: Invalid value: "app-role": spec.target.hosts[0].vars[0].name in body should match '^[A-Za-z_][A-Za-z0-9_]*$'

Имена групп подчиняются тому же правилу и отклоняются таким же образом.

Дополнительно запрещены две группы имён.

Первая группа — имена, которые модуль устанавливает самостоятельно. Такие имена отклоняются во всех полях, где объявляется переменная, поскольку для каждого из них в API уже предусмотрено отдельное поле, а два источника одного значения приводят к ошибкам аутентификации, которые сложно диагностировать. Соответствие имён и полей приведено в таблице ниже.

Переменная Поле, в котором задаётся значение
ansible_host hosts[].address или status.ipAddress машины
ansible_user Ключ username Secret подключения
ansible_password, ansible_ssh_pass Ключ password
ansible_ssh_private_key_file Ключ ssh-privatekey
ansible_become_password, ansible_become_pass Ключ become-password
ansible_connection spec.connection.type

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

Остальные переменные с префиксом ansible_ в манифесте задания задавать можно, например ansible_port, ansible_python_interpreter, ansible_become_user, ansible_become_method, ansible_shell_type и ansible_ssh_common_args.

Вторая группа — magic-переменные Ansible, перечисленные в документации Ansible, например groups, hostvars, inventory_hostname, playbook_dir, role_path и omit. Такие имена отклоняются во всех полях задания. Значения magic-переменных Ansible заполняет самостоятельно, поэтому их присвоение либо не даёт эффекта, либо нарушает вычисление выражений. Отдельным случаем является переменная omit, значение которой означает «не передавать этот параметр». Переменная с таким именем изменила бы поведение каждой задачи, где используется omit, и ошибка при этом не возникла бы.

Места, в которых выполняются проверки имён, приведены в таблице ниже.

Источник имени Имя не является идентификатором Имя входит в одну из двух групп выше
spec.playbook.vars, hosts[].vars Команда d8 k apply возвращает ошибку Задание завершается с причиной InvalidVariables до создания пода
Документ varsFiles Не проверяется, поскольку контроллер не читает файл Не проверяется
Аннотация машины Задание завершается с причиной InvalidInventoryAnnotations Задание завершается с той же причиной. Дополнительно запрещены все переменные с префиксом ansible_, кроме ansible_port

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

status:
  phase: Error
  message: 'The variable "omit" in spec.playbook.vars[0] cannot be used: it shadows an Ansible magic variable, which Ansible fills in itself.'
  conditions:
    - type: Completed
      status: "False"
      reason: InvalidVariables

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

The variable "ansible_user" in spec.playbook.vars[0] cannot be used: the module sets it
itself, from the username key of the connection Secret.

Запуск по расписанию

Расписание выполняет плейбук по времени так же, как ресурс CronJob запускает объекты Job. На каждый тик cron создаётся отдельное задание со своим статусом. Расписание описывается ресурсом AnsibleRunSchedule:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRunSchedule
metadata:
  name: nightly-hardening
  namespace: demo
spec:
  schedule: "0 4 * * *"
  timeZone: "Europe/Moscow"        # по умолчанию UTC
  concurrencyPolicy: Forbid        # Forbid (по умолчанию) | Allow | Replace
  successfulRunsHistoryLimit: 3
  failedRunsHistoryLimit: 1
  startingDeadlineSeconds: 300     # тик, пропущенный дольше этого, отбрасывается
  suspend: false
  template:                        # AnsibleRun, создаваемый на каждый тик
    spec:
      target: { type: VirtualMachines, virtualMachines: { selector: { matchLabels: { role: worker } } } }
      connection: { secretRef: { name: ssh-creds } }
      playbook: { type: Git, git: { url: https://github.com/example-org/infra, path: playbooks/site.yml } }

Работа расписания подчиняется следующим правилам:

  • Имя задания складывается из имени расписания и времени тика в формате Unix, например nightly-hardening-1735660800. Старые задания удаляются по лимитам истории, а вместе с расписанием удаляются все его задания.
  • Спецификация расписания, включая шаблон задания, редактируется, в отличие от неизменяемой спецификации отдельного AnsibleRun. Изменения действуют со следующего тика, а текущее задание продолжает работу по прежнему шаблону.
  • Настройка concurrencyPolicy: Forbid пропускает тик, пока предыдущее задание активно. Пропущенные тики не выполняются задним числом.
  • Настройка suspend: true приостанавливает создание новых заданий. Уже созданные задания при этом продолжают работу.
  • Некорректное расписание или часовой пояс отражаются в условии Ready с причиной InvalidSchedule. Задания по такому расписанию не создаются, пока ошибка не исправлена.

Результат задания

Задание записывает в свой статус фазу выполнения, счётчики задач по каждому хосту и список задач, завершившихся с ошибкой.

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

d8 k get ansibleruns -n demo
d8 k get ansiblerun configure-vm -n demo -o yaml
status:
  phase: PlaybookSucceeded
  message: Run completed successfully   # при ошибке — ещё и имя пода с полным выводом
  hosts:                                # по записи на хост, как в PLAY RECAP
    - name: demo-vm-01
      address: 10.66.10.2
      ok: 3
      changed: 1
      rescued: 1                        # ошибки, обработанные блоком rescue
      ignored: 0                        # ошибки под ignore_errors
  summary: { total: 1, successful: 1, failed: 0, unreachable: 0, skipped: 0 }
  failures: []                          # задачи с ошибкой и их вывод, до 20
  podRef: { name: d8a-ar-configure-vm-x7k2p, namespace: demo }
  playbookCommit: 82b6d90a…             # только для источника Git
  conditions:
    - type: Completed
      status: "True"
      reason: PlaybookSucceeded

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

Фаза Значение
Pending Выполняется проверка зависимостей и целей задания
Running Под раннера создан. Загружается образ, выполняются init-контейнеры, затем плейбук
PlaybookSucceeded Плейбук выполнен успешно, под раннера удалён
PlaybookFailed Плейбук выполнялся и сообщил об ошибках задач или о недоступных хостах
Error Выполнение плейбука не началось, поэтому вывод Ansible отсутствует

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

Ошибки, которые плейбук обработал самостоятельно с помощью блока rescue или параметра ignore_errors, попадают в счётчики rescued и ignored. Плейбук при этом остаётся успешным, а список status.failures — пустым. В логе Ansible такие ошибки по-прежнему выводятся с префиксом fatal:, поэтому отслеживать их следует по перечисленным счётчикам.

Диагностика

Диагностику задания следует начинать с фазы выполнения и причины в условии Completed. Поле status.message содержит тот же текст, а при ошибке дополнительно указывает под раннера, в логах которого находится полный вывод плейбука.

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

Причина Описание Действия
DependencyMissing / DependencyInvalid Нет Secret или ConfigMap, либо в объекте нет названного ключа — учётных данных подключения, доступа к Git, ключа valueFrom переменной В сообщении названо, чего именно
PlaybookConfigMapMissing / PlaybookConfigMapInvalid Нет ConfigMap с плейбуком или нужного ключа в нём Создайте; отсутствующий ConfigMap оставляет задание в Pending
InvalidSelector Селектор целей пуст или некорректен Задайте лейблы или имена явно
InvalidInventoryAnnotations Недопустимое имя переменной или группы на машине В сообщении названы машина и аннотация
InvalidVariables Переменную из vars или hosts[].vars использовать нельзя: непригодное имя, запись с обоими или ни одним из value и valueFrom, объект с пустым именем, два имени, требующие одной переменной окружения В сообщении названы поле и переменная; правила — в разделе Запрещённые имена переменных
VarsSourceNotFound Secret или ConfigMap с переменными не существует. Не терминальная причина: задание ждёт в Pending Создайте объект либо пометьте источник optional: true, как описано в разделе Отсутствующий источник переменных
NoEligibleTargets Ни одна цель не подошла status.skippedHosts объясняет по каждой
NetworkNotFound Такой Network или ClusterNetwork нет Проверьте имя и тип; проектная сеть видна только в своём неймспейсе
NetworkWithoutIPAM У сети нет пула адресов Привяжите пул к сети либо задайте цели типа Hosts
GitCloneFailed Не удалось загрузить репозиторий d8 k logs <POD_NAME> -c git-clone
GalaxyInstallFailed Не удалось установить содержимое Galaxy d8 k logs <POD_NAME> -c galaxy-install

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

Причина в skippedHosts Описание
TargetNotReady / TargetNotRunning Машина запускается (Pending, Starting) или не работает вовсе (Stopped, Terminating, Failed)
NoAddress У машины пока нет адреса в основной сети
NetworkNotAttached Сеть, указанная в задании, к этой машине не подключена
NoAddressInNetwork Сеть подключена, но адреса в ней пока нет
TargetNotFound Имени из names не существует
TargetBusy Машину использует другое активное задание (его имя — в busyBy)

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

Причина Описание Действия
PlaybookFailed Задачи завершились с ошибкой или хосты оказались недоступны Просмотрите список status.failures, затем логи пода
AnsibleRunnerPodFailed Под раннера завершился, не оставив результата, пригодного для разбора Просмотрите логи пода. Под при этом сохраняется
RunFailed Задание завершилось с ошибкой без более точной причины Просмотрите логи пода

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

Симптом Порядок разбора
Задача получает строку там, где ожидался список или число Значение передано через valueFrom, а в отдельном ключе хранится строка, как описано в разделе Тип значения из отдельного ключа. Набор значений со структурой следует передавать файлом
Переменная, заданная в задании, не влияет на выполнение То же имя задано другим значением на 22-м уровне приоритета, либо плейбук читает другое имя. Проверьте приоритет значений
Хосты в состоянии unreachable Проверьте подключение по SSH: пользователя и ключ или пароль в Secret, а также доступность гостевой ОС по этому адресу. Для дополнительной сети проверьте, настроил ли DHCP-клиент интерфейс
Фаза PlaybookFailed Список status.failures указывает хост, задачу и текст ошибки, а полный вывод находится в логах пода
Задание долго остаётся в фазе Pending Условие DependenciesReady указывает объект, которого ожидает задание
Под раннера остаётся в состоянии ContainerCreating Не удалось настроить сеть задания. Команда d8 k describe pod покажет причину, полученную от модуля sdn

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

d8 k logs -n demo -l ansible.deckhouse.io/owner=configure-vm

В список status.failures попадает до 20 ошибок задач. Если ошибок больше, полный вывод остаётся в логах пода, который сохраняется до удаления объекта AnsibleRun. Успешное задание сохраняет весь результат в статусе, поэтому его под удаляется сразу.

Такой порядок очистки следует учитывать в двух случаях:

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

Сбор логов в хранилище

Полный вывод утилиты ansible-playbook остаётся в поде раннера, а под успешного задания удаляется через несколько секунд после завершения. Чтобы вывод сохранялся, настройте его сбор модулем log-shipper. В этом случае лог остаётся доступным и после удаления пода.

Поды раннера не являются системными и создаются в неймспейсе задания, поэтому готовые конфигурации сбора логов платформы их не отбирают. Такие конфигурации охватывают только неймспейсы с лейблом heritage: deckhouse, поэтому для сбора логов задания требуется собственная конфигурация.

Настройка сбора в проекте

Ресурс PodLoggingConfig является проектным, поэтому его может создать владелец проекта. Для отбора подов используется лейбл, который контроллер устанавливает на каждый под раннера. Имя пода для отбора не подходит, поскольку складывается из имени задания и хеша и уникально для каждого задания, например d8a-ar-configure-vm-x7k2p.

apiVersion: deckhouse.io/v1alpha1
kind: PodLoggingConfig
metadata:
  name: ansible-runners
  namespace: demo
spec:
  labelSelector:
    matchLabels:
      app.kubernetes.io/managed-by: ansible-controller
  clusterDestinationRefs:
    - d8-loki

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

d8 k get clusterlogdestinations

Настройка сбора в кластере

Приёмник (ClusterLogDestination) и сбор по всем проектам (ClusterLoggingConfig) являются кластерными ресурсами, поэтому создать их может только администратор кластера. Такая конфигурация настраивается один раз на весь кластер и не требует отдельной конфигурации в каждом проекте:

apiVersion: deckhouse.io/v1alpha2
kind: ClusterLoggingConfig
metadata:
  name: ansible-runner-logs
spec:
  type: KubernetesPods
  kubernetesPods:
    labelSelector:
      matchLabels:
        app.kubernetes.io/managed-by: ansible-controller
  destinationRefs:
    - d8-loki

Приёмник для длительного хранения логов создаётся администратором кластера. Встроенный в платформу Loki рассчитан на оперативный разбор логов, а не на их архивное хранение. Срок хранения в нём задаётся параметром retentionPeriodHours модуля loki, а место освобождается по мере заполнения диска, поэтому фактическая глубина хранения зависит от объёма записи.

Если логи заданий требуется хранить месяцами, создайте отдельный ресурс ClusterLogDestination для внешнего хранилища. Модуль log-shipper поддерживает приёмники Loki, Elasticsearch, Logstash, Vector, Kafka, Splunk и Socket. Один и тот же сбор логов можно направить сразу в несколько приёмников, перечислив их в поле destinationRefs.

Поиск лога задания в Loki

После настройки сбора вывод задания попадает в поток с лейблами неймспейса, пода и контейнера, где контейнер имеет имя ansible. Ниже приведены запросы LogQL для типовых задач:

# все задания в неймспейсе
{namespace="demo", container="ansible"}

# одно задание
{namespace="demo", pod=~"d8a-ar-configure-vm-.+"}

# все задания, созданные одним расписанием
{namespace="demo", pod=~"d8a-ar-nightly-baseline-.+"}

# только итоги: строка PLAY RECAP несёт ok/changed/failed по каждому хосту
{namespace="demo", container="ansible"} |= "PLAY RECAP"

# задачи с ошибкой и недоступные хосты
{namespace="demo", container="ansible"} |~ "fatal:|UNREACHABLE"

# сколько заданий падало за сутки
sum by (pod) (count_over_time({container="ansible"} |= "fatal:" [24h]))

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

Числовые пределы

Числовые пределы одного задания приведены в таблице ниже.

Ограничение Значение
Встроенный плейбук 64 КБ
Хосты, заданные адресом 1000 на задание; 64 переменные и 32 группы на хост
Теги По 64 записи в spec.playbook.tags и spec.playbook.skipTags
Переменные 64 записи в spec.playbook.vars; 8 файлов в spec.playbook.varsFiles
status.failures До 20 ошибок задач, остальные ошибки остаются в логах пода
Дополнительные сети Только IPv4