Примеры работы с Ansible

Обновлено Обновлено: Опубликовано Опубликовано:

Используемые термины: Ansible.

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

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

Получение информации

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

1. Сбор общей информации о системе.

Чтобы собрать информацию о системе, на которой выполняется сценарий ansible, нужно использовать модуль setup, например:

- name: Get service facts
  setup:

Или можно применить фильтр, чтобы сбор выполнялся быстрее:

- name: Get service facts
  setup:
    filter: 'ansible_os_family'

Данная информация будет записана в переменную ansible_facts. При желании, ее значение можно отобразить с помощью модуля debug:

- name: Print all available facts
  debug:
    var: ansible_facts

Также мы можем обратиться к конкретному элементу массива ansible_facts, получив информацию о конкретной настройке или опции:

    ...
    ansible_facts.hostname

О setup: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html.

2. Получить определенную информацию о компьютере.

Выполняется с помощью различных модулей с окончанием _facts. Рассмотрим примеры.

а) список сервисов. Для этого существует service_facts:

- name: Populate service facts
  ansible.builtin.service_facts:

- name: Print all available services
  debug:
    var: ansible_facts.services

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

О service_facts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/service_facts_module.html.

б) установленные пакеты. Используем package_facts:

- name: Gather the package facts
  package_facts:
    manager: auto

- name: Print all available packages
  debug:
    var: ansible_facts.packages

О package_facts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_facts_module.html.

3. Отображение на экран переменной.

Выше мы уже использовали debug для отображения переменной — принцип тот же:

- name: Show Value of Variable
  debug:
    msg: "{{ variable }}"

* при выполнении задачи на экране мы увидим значение переменной variable. Обратите внимание, что запись ansible.builtin.debug и debug — это одно и то же, то есть, ansible.builtin можно не писать.

О debug: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/debug_module.html.

4. Сохранение результата при выполнении задания.

Создать и использовать переменную можно в процессе выполнения работы с помощью директивы register:

- name: Run a shell command and register its output as a variable
  shell: command
  register: command_result

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

- name: Set variable
  set_fact:
    my_hosts: "{{ command_result.stdout }}"

* в данном случае мы взяли переменную, которую получили в примере выше и разбили строку по знаку ":". Таким образом, мы получим массив данных, который будет сохранен в переменную my_hosts.

Работа с переменными

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

1. Переменные в ролях.

Для описания переменных в ролях существуют два каталога — vars и defaults. Последние имеют самый низкий приоритет, это значит, что их можно переопределить.

Независимо от каталога, в котором идет описание переменной, синтаксис один:

hostname: mail.dmosk.local

* в данном примере определена переменная hostname со значением mail.dmosk.local.

2. Переменные в файле inventory.

Инвентарный файл используется для описания хостов, которые могут быть задействованы при работе сценариев ansible. Переменная может быть определена на уровне всех хостов или на уровне конкретного хоста:

all:
  vars:
    tower_user_name: master
  hosts:
    192.168.0.10:
      human_name: mail
    192.168.0.11:
      human_name: www

* в нашем примере будут определены 3 переменные:

  • tower_user_name для всех машин.
  • human_name для 192.168.0.10.
  • human_name для 192.168.0.11.

3. Переменные в плейбуке.

Задаются в файле плейбука:

- hosts: all
  ...
  vars:
    vm_name: test-vm

* в данном примере это переменная vm_name.

4. При запуске плейбука.

Когда мы запускаем на выполнение наш плейбук, мы можем передать переменные с помощью опции extra-vars:

ansible-playbook ... --extra-vars "{ 'address':'dmosk.local' }"

* передаем переменную address.

5. Системные переменные.

Мы можем использовать не только определенные нами переменные, но и вытаскивать системные переменные. Для этого используется плагин lookup вместе с модулем env:

- name: Show Value of System Variable
  debug:
    var: lookup('env', 'HOME')

* в данном примере мы получим значение системной переменной HOME (домашней директории пользователя, под которым запущен сценарий ansible).

Или можно записать ее в переменную, чтобы использовать в других задачах:

- name: Set sys_home From System Variable
  set_fact:
    sys_home: "{{ lookup('env', 'HOME') }}"

- name: Show Value of sys_home Variable
  debug:
    var: sys_home

О lookup + env: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/env_lookup.html.

Проверки, условия и действия на основе этих проверок

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

1. Проверка на пустую папку.

Задачи сводится к двум операциям:

  • получении списка файлов в целевом каталоге (например, с помощью команды ls) и регистрации полученного значения в переменную с помощью register.
  • проверка содержимого переменной, которую мы получили на шаге 1 с помощью when.

Пример будет таким:

- name: Register Contents of PGDATA Folder
  shell: ls /var/lib/postgresql/11/main
  register: pg_contents

- name: Init PostgreSQL DB
  shell: /opt/pgpro/std-11/bin/pg-setup initdb
  environment:
    PGDATA: "/var/lib/postgresql/11/main"
  when: pg_contents["stdout_lines"] | length == 0

* в данном примере мы в первой задаче выводим содержимое каталога /var/lib/postgresql/11/main и помещаем его в переменную pg_contents. Во второй задаче мы уже проверяем с помощью when количество строк — если их 0, то тогда выполняем команду initdb. На практике, это важно, так как инициализация базы PostgreSQL при непустом каталоге выводит сообщение об ошибке.

О when: https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html.

2. Проверить, определена ли переменная.

Для этого используется опция is defined (определена) или is not defined (не определена):

when: pgpro is defined

when: pgpro is not defined

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

В официальной документации про это сказано в статье о when (ссылка выше).

3. Выполнение команды, если сервис в рабочем состоянии.

Нам необходимо получить информацию о службах с помощью service_facts, после чего можно уже делать проверку с помощью when:

- name: Populate service facts
  ansible.builtin.service_facts:

- name: Stop Service If Running One
  shell: systemctl stop apache2
  when: ansible_facts.services["apache2.service"].state | default(false) == "running"

* в данном примере мы проверим, есть ли служба apache2 и запущена ли она. Если это так, то мы ее останавливаем.

Подробнее о service_facts можно прочитать в документации (ссылка выше в разделе 4. Получить список сервисов).

4. Существует ли файл.

Проверка может быть выполнена с помощью stat:

- name: Register File Stat
  stat:
    path: /etc/nginx/nginx.conf
  register: stat_result

- name: Cat file if exists
  shell: cat /etc/nginx/nginx.conf
  when: stat_result.stat.exists

* в данном примере будет выполнено чтение файла /etc/nginx/nginx.conf, если он существует.

О stat: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/stat_module.html.

5. Операционная система.

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

а) Проверка по семейству:

- name: Run task1 if OS is Debian
  ...
  when: ansible_os_family == "Debian"

- name: Run task2 if OS is Red Hat
  ...
  when: ansible_os_family == "RedHat"

* в данном примере мы запустим задачу task1 в системах на основе Debian и task2 — на основе RedHat.

б) Проверка по версии системы:

- name: Run task1 if OS is Debian 9
  ...
  when: ansible_os_family == "Debian" and ansible_distribution_major_version == "9"

- name: Run task2 if OS is Red Hat 8
  ...
  when: ansible_os_family == "RedHat" and ansible_distribution_major_version == "8"

- name: Run task3 if OS is Red Hat less 8
  ...
  when: ansible_os_family == "RedHat" and ansible_distribution_major_version | int < 8

* в данном примере мы запустим задачу task1 в системах на основе Debian версии 9, task2 — на основе RedHat версии 8 и task3 на системах ниже 8 версии.

6. Выдать ошибку и остановить выполнение.

Для этого предусмотрен модуль fail или опция задания failed_when. Рассмотрим примеры использования обоих.

а) Модуль fail. Является отдельным заданием. Его можно выполнить просто так, но без условия его применение нелогично. Пример:

- name: Stop the executing if variable is empty
  fail:
    msg: The variable srv_ip is not defined or its empty.
  when: srv_ip is not defined or srv_ip == ""

* в нашем примере, если переменная srv_ip окажется пустой или неопределенной, мы получим ошибку с сообщением, определенным в опции msg.

О fail: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fail_module.html.

б) Опция failed_when. Задает условие, при соблюдении которого задание выполняется с ошибкой, например:

- name: Fail task when the command error output prints FAILED
  ansible.builtin.command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: "'FAILED' in command_result.stderr"

* данный пример с официального сайта ansible (ссылка ниже). Задание выдаст ошибку, если в результате выполнения скрипта /usr/bin/example-command будет текст FAILED.

О failed_when: https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html.

7. Ожидания с помощью wait_for.

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

  • Просто ждать определенное время.
  • Удачное соединение по сети с узлом.
  • Определенное содержимое в файле.
  • Пока не появится или удалится файл или папка.

О wait_for https://docs.ansible.com/ansible/latest/collections/ansible/builtin/wait_for_module.html.

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

а) Ожидаение файла:

- name: Wait rpm file
  wait_for:
    path: /tmp/build/my_package.rpm

* в данном примере выполнение плейбука остановится, пока по пути /tmp/build/my_package.rpm не появится файл.

б) Ждем, пока хост не станет доступен по SSH:

- name: Ждем, пока не сможем подключиться к хосту по SSH
  wait_for:
    host: 192.168.1.11
    port: 22
    state: "started"
    timeout: 180

8. Проверка условий с выводом ошибки или успеха.

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

а) Значение переменной my_key должно быть от 0 до 100:

- name: Значение переменной my_key должно быть от 0 до 100
  assert:
    that:
      - my_key <= 100
      - my_key >= 0
    fail_msg: "Неправильное значение my_key - должно быть от 0 до 100"
    success_msg: "Значение my_key верное"

б) Значение my_login не должно быть только из цифр:

  - name: Проверяем название датасета
    assert:
      that:
        - my_login is not regex("^[0-9]+$")
      fail_msg: "Имя my_login не может состоять только из цифр"
      success_msg: "Имя my_login задано верно"

О assert: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/assert_module.html.

Работа с репозиториями

Рассмотрим настройку репозиториев для систем на базе DEB и RPM.

Работа с репозиториями RPM

Ведется с помощью модуля yum_repository.

О yum_repository: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/yum_repository_module.html.

1. Добавление репозитория:

- name: "Add YUM repository"
  yum_repository:
    name: new_yum_repo
    description: "New Yum Repo Description"
    file: new_repo_file
    baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/
    enabled: yes
    gpgcheck: no
    validate_certs: no

2. Для удаления репозитория также используем yum_repository

- name: "Remove YUM repositories"
  yum_repository:
    name: pgsql95
    state: absent

* в нашем примере будет удален репозиторий с названием pgsql95.

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

- name: "Remove YUM repositories"
  yum_repository:
    name: pgsql95
    state: absent
    file: pgsql

3. Установка пакетов из dnf-модулей. Начиная с версии 8 дистрибутивов на основе RPM пакеты могут устанавливаться из модульных репозиториев, в которых идет четкое разграничение между версиями пакетов. В ansible, если необходимо установить пакет из определнного модуля, используем сценарий на подобие:

- name: 
  dnf:
    name:
      - @postgresql:13/client
      - @postgresql:13/server
    state: present

* в данном примере мы установим пакеты postgresql-client и postgresql-server из модульного репозитория postgresql:13.

Работа с репозиториями DEB

1. Добавление репозитория:

По ссылке:

- name: "Add DEB repository"
  apt_repository:
    repo: deb http://dl.google.com/linux/chrome/deb/ stable main
    filename: new_deb_repo

С использованием персонального архива пакетов (PPA):

- name: "Add DEB PPA"
  apt_repository:
    repo: ppa:deadsnakes/ppa

О apt_repository: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_repository_module.html.

2. Для удаления репозитория нужно воспользоваться директивой state со значением absent, например:

- name: "Remove Debian default repository"
  apt_repository:
    repo: deb https://ftp.debian.org/debian stretch main contrib
    state: absent

* в данном примере будет удален репозиторий, который идет по умолчанию для Debian Stretch.

3. Импорт ключа.

Рассмотрим примеры с добавлением gpg-ключа по URL и с сервера ключей:

  - name: Import postgresql repo key
    apt_key:
      url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
      state: present

* импорт ключа из файла https://www.postgresql.org/media/keys/ACCC4CF8.asc.

  - name: Import keyserver.ubuntu.com repo keys
    apt_key:
      keyserver: keyserver.ubuntu.com
      id: 648ACFD622F3D138
      state: present

* импорт ключа с идентификатором 648ACFD622F3D138 из сервера ключей keyserver.ubuntu.com.

О apt_key: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_key_module.html.

4. Обновление кэша репозитория.

Выполняется с помощью модуля apt и опции update_cache:

- name: Update repositories cache
  apt:
    update_cache: yes

Об apt: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html.

Установки пакетов, модулей и расширений

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

  • Установки пакетов в систему.
  • Загрузки исходников.
  • Установке дополнительных модулей.
  • Распаковке архивов.

Обратите внимание на опцию state. С ее помощью мы можем управлять поведением установки:

  • present — пакет должен быть установлен.
  • latest — должна быть установлена последняя версия.
  • absent — пакет должен быть удален.

Рассмотрим это подробнее.

1. Установка/удаление пакетов в систему.

Выполняется с помощью универсального модуля package или с помощью специализированных yum и apt. Последние имеют больше полезных опций.

а) package:

- name: Install NTP-client
  package:
    name: chrony
    state: latest

* по данной инструкции в нашей системе должен быть установлен пакет chrony последней версии.

Для установки пакета конкретной версии, ее нужно указать в названии пакета:

- name: Install NTP-client
  package:
    name: chrony-3.4
    state: present

О package: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_module.html.

б) dnf/yum.

Установка:

- name: Install NTP-client
  yum:
    name: chrony
    state: present

Для удаления:

- name: Remove NTP-client
  yum:
    name: chrony
    state: absent

Обновление:

- name: Обновить пакеты
  yum:
    name: '*'
    state: latest

А так можно сделать даунгрейд:

- name: Downgrade NTP-client
  yum:
    name: chrony-1.0.0
    state: present
    allow_downgrade: true

О yum: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/yum_module.html.

в) apt.

Установка:

- name: Install NTP-client
  apt:
    name: chrony
    state: present

Если нам нужно установить пакет из файла deb, синтаксис будет следующий:

- name: "Install lib for postgresql"
  apt:
    deb: http://ftp.ru.debian.org/debian/pool/main/l/llvm-toolchain-7/libllvm7_7.0.1-8+deb10u2_amd64.deb
    state: present

* в данном примере мы устанавливаем пакет libllvm7 из файла deb, который доступен по url http://ftp.ru.debian.org/debian/pool/main/l/llvm-toolchain-7/libllvm7_7.0.1-8+deb10u2_amd64.deb.

Выполнить только обновление установленных пакетов:

- name: Установка обновлений
  apt:
    name: "*"
    state: latest
    update_cache: true
    cache_valid_time: 3600

Об apt: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html.

2. Установка модуля в nodejs.

Установка модулей в nodejs выполняется с помощью npm. Для него в ansible есть отдельная функция:

- name: Install nodejs modules.
  npm:
    name: newman
    global: yes

* в данном примере будет выполнена установка newman, которая будет доступна всем проектам (опция global).

О nodejs npm: https://docs.ansible.com/ansible/latest/collections/community/general/npm_module.html.

3. Установка расширений для python.

Используем модуль pip. Рассмотрим несколько примеров.

а) установка пакета python:

- name: Pip install psycopg2
  pip:
    name: psycopg2
    state: present

* в данном примере будет установлен psycopg2.

б) обновление пакетов:

  - name: Upgrade pip and wheel
    pip:
      name: "{{ item }}"
      extra_args: --upgrade
      executable: pip3
    loop:
      - pip
      - wheel

* в нашем примере будут обновлены пакеты pip и wheel.

в) использовать определенную версию pip:

  - name: Install python modules with pip3
    pip:
      name: patroni[consul]
      executable: pip3
      state: present

О pip: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/pip_module.html.

4. Распаковка архива.

Выполняется с помощью unarchive:

- name: Unpacking Nginx Source
  unarchive:
    src: "http://nginx.org/download/nginx-{{ nginx_ver }}.tar.gz"
    dest: /tmp/
    remote_src: yes
    creates: /tmp/nginx-{{ nginx_ver }}.tar.gz

* в данном примере мы распакуем исходник для nginx в каталог /tmp. Обратите внимание на две вещи:

  • Мы используем переменную nginx_ver. Данная переменная должна быть определена при запуске плейбука, или в инвентарном файле, или в var, или в default. Подробнее в соответствующем разделе выше.
  • Опция creates позволит не выполнять операцию, если существует файл /tmp/nginx-{{ nginx_ver }}.tar.gz.

Об unarchive: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/unarchive_module.html.

Настройка системы

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

1. Добавить задание в cron.

Выполняется с помощью модуля cron:

- name: Add Job for Run Command
  cron:
    name: Start Script
    job: "/scripts/command.sh"
    user: root
    minute: "0"
    hour: "*/6"
    day: "*"
    month: "*"
    weekday: "*"

* в данном примере мы создадим задание для запуска команды /scripts/command.sh каждый день, каждые 6 часов.

О cron: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/cron_module.html.

2. Создание учетной записи.

Для этого есть модуль user. У него много опций, рассмотрим некоторые из них.

а) Простая учетная запись:

- name: Create User1
  user:
    name: user1
    shell: /bin/bash
    create_home: yes

* в данном примере мы создадим пользователя user1 с домашней директорией. Также мы указали для использования командную оболочку /bin/bash.

б) Для создания системной учетной записи нам достаточно:

- name: Create User Consul
  user:
    name: consul
    system: yes
    comment: "Consul Agent"

* в данном примере будет создана учетная запись consul.

в) Создаем пользователя с паролем:

- name: Create User2
  user:
    name: user2
    shell: /bin/bash
    create_home: yes
    password: "{{ 'my_passw0rd' | password_hash('sha512') }}"

* будет создан пользователь user2 с паролем my_passw0rd.

г) Добавляем пользователя в группу:

- name: Добавляем пользователя clamav в группу amavis
  user:
    name: vmail
    groups: mail
    append: yes

* в данном примере пользователь vmail будет добавлен в группу mail.

О user: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html.

д) Создание группы:

- name: Создаем группу vmail
  group:
    name: vmail
    state: present

О group: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/group_module.html.

3. Работа с systemd.

Для данной настройки есть одноименный модуль systemd. Рассмотрим варианты его использования.

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

- name: systemd reload
  systemd:
    daemon_reload: yes

б) разрешить сервис (автозапуск):

- name: mysql enable
  systemd:
    name: mysql
    enabled: yes

* для сервиса mysql.

в) перезапустить сервис:

- name: mysql reload
  systemd:
    name: mysql
    state: restarted

г) остановить сервис:

- name: mysql stoped
  systemd:
    name: mysql
    state: stopped

О systemd: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/systemd_module.html.

4. Настройка брандмауэра.

Выполняется разными модулями в зависимости от используемой системы управления netfilter:

  • firewalld
  • iptables
  • ufw

Рассмотрим небольшие примеры.

а) firewalld:

- name: permit traffic in default zone for https service
  firewalld:
    service: https
    permanent: yes
    state: enabled

Подробнее: https://docs.ansible.com/ansible/latest/collections/ansible/posix/firewalld_module.html.

б) iptables:

- name: Block specific IP
  iptables:
    chain: INPUT
    source: 8.8.8.8
    jump: DROP

Подробнее: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/iptables_module.html.

в) UFW.

Добавить 80 порт:

- name: Allow all access to tcp port 80
  ufw:
    rule: allow
    port: '80'
    proto: tcp

Добавить порты с циклом:

- name: Allow Ports in Firewall
  ufw:
    rule: allow
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
    comment: "{{ item.comment }}"
  loop:
    - { port: 5432, proto: tcp, comment: 'PostgreSQL' }

Подробнее: https://docs.ansible.com/ansible/latest/collections/community/general/ufw_module.html.

5. Имя компьютера.

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

- name: Задаем имя компьютера
  hostname:
    name: myweb
    use: systemd

* в данном примере мы задаем имя myweb. Обратите внимание на опцию use — в зависимости от операционной системы или ее версии, ее значение должно отличаться.

О hostname: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/hostname_module.html.

6. Часовой пояс.

Часовой пояс можно настроить с помощью модуля timezone, например:

- name: Задаем часовой пояс
  timezone:
    name: Europe/Moscow

О timezone: https://docs.ansible.com/ansible/latest/collections/community/general/timezone_module.html.

Работа с папками и файлами

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

1. Создание каталогов и файлов.

Создание файлов и каталогов выполняется с помощью модуля file.

а) для каталога в качестве state указываем directory:

- name: Create Directories
  file:
    path: "{{ item }}"
    state: directory
    owner: www-data
    group: www-data
    mode: 0755
  loop:
    - '/var/www/site1'
    - '/var/www/site2'

* в данном примере мы создадим 2 каталога: site1 и site2 в каталоге /var/www.

б) для создания файла убираем опцию state (или даем ей значение touch):

- name: Create File
  file:
    path: "/var/www/site1/index.php"
    state: touch
    owner: www-data
    group: www-data
    mode: 0644

* в данном примере мы созданим файл index.php в каталоге /var/www/site1.

в) для создания симлинка используем state со значением link:

- name: Create a symbolic link from foo to bar
  file:
    src: /usr/bin/foo
    dest: /usr/sbin/bar
    state: link

О file: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_module.html.

2. Задать права.

Это можно выполнить с помощью модуля создания файла или каталога:

- name: Set File Rights
  file:
    path: "/var/www/site1/index.php"
    owner: www-data
    group: www-data
    mode: 0664

* обратите внимание, это пример из предыдущено раздела. Для созданного файла мы просто немного изменили права.

3. Копирование файлов из каталога.

Для копирования данных мы используем модуль copy:

- name: Copy Cert File If Different
  copy:
    src: "{{ item }}"
    dest: /etc/ssl/dmosk
    remote_src: no
    mode: 0644
    owner: root
    group: root
  with_fileglob:
    - files/*

* в данном примере мы прочитаем все содержимое каталога files на компьютере с ansible, и скопируем его в каталог /etc/ssl/dmosk на целевом компьютере.

О copy: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/copy_module.html.

4. Загрузка файла на хост ansible.

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

- name: Загрузка файла на ansible хост
  fetch:
    src: /tmp/newcert
    dest: /tmp/
    flat: yes

Подробнее о fetch: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fetch_module.html.

5. Используем шаблон.

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

- name: Create Config for Consul Agent
  template:
    src: templates/consul/config.json.j2
    dest: /etc/consul.d/config.json

* в данном примере мы возьмом шаблон templates/consul/config.json.j2 на компьютере ansible и разместим его в по пути /etc/consul.d/config.json на целевом компьютере.

Мы можем вывести в консоль результат обработки шаблона следующим образом:

- name: Show Templating Results
  debug:
    msg: "{{ lookup('template', './config.json.j2') }}"

О template: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html.

6. Архивирование.

Создать архив из файла или каталога можно с помощью модуля archive:

- name: "Use gzip to compress folder"
  archive:
    path: /etc/raddb
    dest: "/tmp/raddb.gz"
    format: gz

* в данном примере мы создадим архив из каталога /etc/raddb и сохраним его в файл /tmp/raddb.gz.

О archive: https://docs.ansible.com/ansible/latest/collections/community/general/archive_module.html.

Для распаковки архивов используется модуль unarchive, о котором мы говорили выше.

7. Поиск файлов и папок.

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

а) Удалить последние 30 файлов. Задача решается в два этапа:

  • ищем содержимое целевого каталога.
  • сотритуем список найденных по времени изменения файлов и удаляем все, что идут после определенного числа объектов.

Поиск выполняем с помощью модуля find, удаление — file:

- name: "Get list of backup files"
  find:
    paths: "/backup"
    file_type: file
  register: founds

- name: "Delete last 30 Copies"
  file:
    path: "{{ item }}"
    state: absent
  loop: "{{ (founds.files | sort(attribute='mtime', reverse=True) | map(attribute='path') | list )[30:] }}"

* в данном примере мы ищем файлы в каталоге /backup, после чего сортируем найденное и удаляем по списку все файлы, которые идут после 30-го.

б) Удаление архивов для логов. Также выполняем в два этапа — более сложный поиск и удаление с помощью file:

- name: "Get a list of logs to be deleted"
  find:
    paths: "/var/log"
    file_type: file
    patterns: '*.gz,*.log-*,*.old,*.[0-9].log,*.log.[0-9],*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
    recurse: yes
  register: logs_to_delete
  
- name: "Delete logs"
  file:
    path: "{{ item }}"
    state: absent
  loop: "{{ logs_to_delete.files | map(attribute='path') | list }}"

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

О find: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/find_module.html.

8. Скачать файл с помощью curl.

Для этого используется модуль uri. Простой пример:

- name: CURL simple download file
  uri:
    url: https://www.dmosk.ru/files/winsetupfromusb.zip
    dest: /tmp

* в данном примере мы загрузим файл https://www.dmosk.ru/files/winsetupfromusb.zip в каталог /tmp.

Пример посложнее:

- name: CURL download file with token auth
  uri:
    url: https://gitlab.dmosk.ru/api/v4/projects/555/repository/files/folder%2Fpath%2Fdata.sql/raw?ref=master
    dest: /tmp/data.sql
    owner: dmosk
    group: dmosk
    mode: 640
    headers:
      PRIVATE-TOKEN: access-token

* в данном примере мы скачаем файл с ресурса, где требуется аутентификация по токену, который передается в заголовке. Заголовки мы передаем с помощью параметра headers. Также мы задаем права на загруженный файл и делаем в качестве владельца пользователя и группу dmosk.

Об uri: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html.

9. Создание временных файлов и папок.

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

Пример создания каталога:

- name: Create temporary ansible directory
  tempfile:
    state: directory
    suffix: ansible
  register: tmp

Путь до созданного каталога будет записан в переменную tmp.path.

После не забываем удалить созданную папку:

- name: Remove temporary ansible directory
  file:
    path: "{{ tmp.path }}"
    state: absent
  when: tmp.path is defined

О tempfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/tempfile_module.html.

10. Работа с GIT.

О Git: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/git_module.html.

а) Клонирование проекта из гита.

Выполняется с помощью модуля git.

- name: Clone docker-compose
  git:
    repo: "https://github.com/docker/compose.git"
    dest: /tmp/docker-compose

* в данном примере мы сделаем клон репозитория в каталог /tmp/docker-compose.

б) Изменение содержимого для файла.

Модуль ansible не поддерживает отпавку изменений в git, но можно воспользоваться API. Например, для gitlab обновить контент файла можно с помощью модуля uri:

- name: 
  uri:
    url: "https://gitlab.dmosk.ru/api/v4/projects/666/repository/files/folder%2Ffilename"
    method: PUT
    return_content: false
    body_format: json
    body:
        branch: "main"
        author_email: "master@dmosk.ru"
        author_name: "Dmitriy Mosk"
        content: "Text for file"
        commit_message: "update filename"
    headers:
      PRIVATE-TOKEN: 00000_1111111_33333
      Content-Type: application/json
    validate_certs: no

* где:

  • url — полный путь до файла. Обратите внимание на:
    • gitlab.dmosk.ru — адрес нашего сервера gitlab.
    • 666 — идентификатор проекта. Его можно посмотреть в настройках самого проекта.
    • folder%2Ffilename — путь до файла. В нормальном формате, это folder/filename.
  • body — содержит данные, которые будут отправлены на сервер для смены контента.
  • headers PRIVATE-TOKEN — токен доступа к API. Его можно создать в настройках профиля учетной записи Gitlab.

Подробнее о работе API в гиблабе: https://docs.gitlab.com/ee/api/repository_files.html.

Содержимое файла

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

1. Просмотр содержимого файла.

Содержимое файлов на удаленном сервере и на стороне ansible просматривается по-разному. Рассмотрим оба варианта.

а) На ansible.

Чтобы увидеть содержимое файла, который лежит на стороне ansible, используем плагин lookup:

- name: Show File Content
  debug:
    msg: "{{ lookup('file', '/etc/ntp.conf') }}"

* в данном примере мы просто покажем содержимое файла /etc/ntp.conf.

О lookup: https://docs.ansible.com/ansible/latest/plugins/lookup.html.

б) На удаленном сервере.

Выполняется в два этапа — просмотр содержимого с сохранением результата в переменную, и вывод содержимого переменной:

- name: Read file content
  shell: cat /path/to/file
  register: file_content

- name: Show file content
  debug:
    var: file_content.stdout

2. Замены строки в файлах.

Замены выполняются с помощью модуля replace. Рассмотрим несколько примеров.

а) Простой поиск и замена строки:

- name: Check configs (comment server address 127.0.0.1)
  replace:
    path: "/etc/nginx/nginx.conf"
    regexp: '^server.address=127.0.0.1$'
    replace: '#server.address=127.0.0.1'

* в данном примере мы добавляем комментарий к строке server.address=127.0.0.1.

б) Замена с подстановкой найденного содержимого:

- name: Check configs (comment server address 127.0.0.1)
  replace:
    path: "/etc/nginx/nginx.conf"
    regexp: '^server.address=(.*)$'
    replace: '# commented for \1'

* в данном примере мы находим строку server.address с любым значением и меняем ее на строку, в которой будет прописано это значение.

О replace: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/replace_module.html.

3. Добавление и удаление строк в файле.

Мы можем менять содержимое файла с помощью модуля lineinfile. Рассмотрим некоторые примеры работы с ним.

а) Удалить строку:

- name: Remove server strings
  lineinfile:
    path: "/etc/chrony.conf"
    regexp: '^server .*'
    state: absent

* в данном примере мы удалим все строки из файла /etc/chrony.conf, которые начинаются на server.

б) Добавить строку:

- name: Add server strings
  lineinfile:
    path: "/etc/chrony.conf"
    line: 'server ntp.server.local'

* в данном примере мы добавим строку server ntp.server.local в файл /etc/chrony.conf. Если данная запись уже есть в файле, ansible ничего не станет менять.

в) Добавить строку с использованием регулярного выражения:

- name: Ensure SELinux is set to enforcing mode
  lineinfile:
    path: /etc/selinux/config
    regexp: '^SELINUX='
    line: SELINUX=enforcing

* пример взят с официального сайта. В данном случае мы гарантируем наличие строки SELINUX=enforcing — либо для директивы SELINUX будет задано определенное значение, либо строка будет полностью вставлена в конфигурационный файл /etc/selinux/config.

О lineinfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/lineinfile_module.html.

4. Разбить строку.

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

- name: Run a shell command and register its output as a variable
  shell: command
  register: command_result

- name: Parse string for dot and show results
  debug:
    var: command_result.stdout.split(".")

* в данном примере мы получим массив данных из предложений.

5. Добавить блок текста.

Можно сделать с помощью модуля blockinfile:

- name: Simple config for PDF converter
  blockinfile:
    path: /opt/app/conf/app.yml
    block: |
      # Simple config
      option:
          key1: value1
          key2: value2
          key3: value3

О blockinfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/blockinfile_module.html.

6. Сохранить содержимое переменной в файл.

Выше мы рассматривали модуль copy для копирования файлов. Мы также можем его использовать для помещения содержимого переменной в файл:

- name: Save my_variable to data.txt file
  copy:
    content: "{{ my_variable }}"
    dest: '/tmp/data.txt'

* в данном примере содержимое переменной my_variable будет сохранено в файле /tmp/data.txt.

Работа с SSL

В данном разделе рассмотрим задачи управления сертификатами и шифрованием при передачи данных.

1. Добавить публичный ключ хоста в known_hosts.

Делается с помощью known_hosts. Пример из официальной документации:

- name: Tell the host about our servers it might want to ssh to
  known_hosts:
    path: /etc/ssh/ssh_known_hosts
    name: foo.com.invalid
    key: "{{ lookup('file', 'pubkeys/foo.com.invalid') }}"

* в данном примере мы добавим ключ из файла pubkeys/foo.com.invalid в /etc/ssh/ssh_known_hosts.

О known_hosts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/known_hosts_module.html.

2. Создание новых SSH-ключей для сервера.

Создание ключей реализуется с помощью модуля openssh_keypair:

- name: Generate New SSH Host Keys
  openssh_keypair:
    path: "/etc/ssh/ssh_host_{{ item.type }}_key"
    owner: root
    state: present
    type: "{{ item.type }}"
    size: "{{ item.size }}"
    force: yes
  loop:
    - { type: dsa, size: 1024 }
    - { type: ecdsa, size: 521 }
    - { type: ed25519, size: 2048 }
    - { type: rsa, size: 2048 }

* в данном примере мы создадим 4 ключа разных типов: dsa, ecdsa, ed25519, rsa. Так как у каждого из них свои требования к размеру, перечень представлен в виде двумерного массива. Ключи будут созданы в каталоге /etc/ssh/.

О openssh_keypair: https://docs.ansible.com/ansible/latest/collections/community/crypto/openssh_keypair_module.html.

3. Работа с SSH authorized_key.

Данный файл содержит публичный ключ для подключения по SSH. Работа с ним через Ansible выполняется с помощью модуля authorized_key.

Пример добавления ключа:

- name: Set authorized key took from file
  authorized_key:
    user: root
    state: present
    key: '{{ item }}'
  with_file:
    - files/key.pub

* в данном примере мы берем содержимое файла files/key.pub и устанавливаем его для пользователя root.

Об authorized_key: https://docs.ansible.com/ansible/2.4/authorized_key_module.html.

4. Создание самоподписанного сертификата.

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

  • openssl_privatekey — создаст приватный ключ.
  • openssl_csr — создаст запрос для выпуска открытого сертификат по заданным параметрам на основе закрытого ключа.
  • openssl_certificate — сгенерирует открытый сертификат на основе ключа запроса.

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

- name: Создаем закрытый ключ (key)
  openssl_privatekey:
    path: /etc/ssl/mycert.key
    size: 2048

- name: Создаем файл запроса (csr)
  openssl_csr:
    path: /etc/ssl/mycert.csr
    privatekey_path: /etc/ssl/mycert.key
    country_name: RU
    locality_name: Russian Federation
    organization_name: Dmosk
    email_address: master@dmosk.ru
    common_name: ansible.dmosk.ru
    subject_alt_name:
      - "DNS:ansible.dmosk.ru"
      - "DNS:test.dmosk.ru"

- name: Создаем самоподписанный сертификат (crt)
  openssl_certificate:
    path: /etc/ssl/mycert.crt
    csr_path: /etc/ssl/mycert.csr
    privatekey_path: /etc/ssl/mycert.key
    provider: selfsigned

* в нашем запросе мы в итоге получим ключи mycert.key и mycert.crt в каталоге /etc/ssl. Результат будет похожим, если бы мы ввели команду openssl req -nodes -new -x509 -keyout /etc/ssl/mycert.key -out /etc/ssl/mycert.crt -subj '/C=RU/L=Russian Federation/O=Dmosk/CN=ansible.dmosk.ru'.

Виртуализация VMware

Работа с виртуальными машинами на платформе VMware выполняется с помощью большого количества различных модулей vmware. С полным их списком можно ознакомиться на странице https://docs.ansible.com/ansible/latest/collections/community/vmware/index.html.

Мы рассмотрим несколько примеров. В них мы будем использовать следующие переменные:

  1. vcenter_hostname — имя сервера vcenter или его IP-адрес.
  2. vcenter_username — логин для подключения к vcenter.
  3. vcenter_password — пароль для подключения. Лучше хранить с использованием vailt.
  4. vcenter_datacenter — имя датацентра, к которому нужно подключаться.

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

1. Модуль vmware_guest

С его помощью мы можем взаимодействовать с гостевой операционной системой виртуальной машины. Необходимо позаботиться, чтобы на последней были установлены VMware Tools.

Подробнее о vmware_guest: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_module.html.

а) Базовое подключение.

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

- name: Connect to ESX Host
  vmware_guest:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    validate_certs: no

* параметр validate_certs, выставленный в no, позволит избежать ошибки, если у нас на хосте используется самоподписанный сертификат (как правило, так и есть).

б) Переименовать виртуальную машину.

Для выполнения действия нам нужно знать идентификатор виртуальной машины:

- name: Rename a virtual machine
  vmware_guest:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    validate_certs: no
    uuid: "{{ hw_product_uuid }}"
    name: "Template-{{ vm_name }}"
    state: present

* где uuid — идентификатор виртуальной машины; name — новое имя виртуальной машины.

в) Конвертировать виртуальную машину в шаблон.

Для этого нужно просто задать признак is_template:

- name: Convert virtual machine to Template
  vmware_guest:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    validate_certs: no
    uuid: "{{ vm_info.instance.hw_product_uuid }}"
    is_template: true
    state: present

2. Модуль vmware_vm_info

Позволяет собирать информацию о виртуальных машинах на хосте VMware.

О модуле vmware_vm_info: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_info_module.html.

а) Получение информации по всем виртуальным машинам на хосте:

- name: Gather all registered virtual machines
  vmware_vm_info:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    validate_certs: no
  register: vminfo

- debug:
    var: vminfo.virtual_machines

* всю информацию мы запишем в переменную vminfo и выведем ее на экран.

б) Чтобы получить информацию не о всех машинах, а о конкретной, то добавляем опцию vm_name:

- name: Gather all registered virtual machines
  vmware_vm_info:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    validate_certs: no
    vm_name: mail-vm
  register: vminfo

3. Модуль vmware_guest_info

Позволяет получить подробную информацию о виртуальной машине.

О модуле vmware_guest_info: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_info_module.html.

а) Получение информации о конкретной виртуальной машине:

- name: Gather virtual machine
  vmware_guest_info:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    datacenter: '{{ vcenter_datacenter }}'
    validate_certs: no
    name: '{{ vm_name }}'
  register: vminfo

- debug:
    var: vminfo.instance

* информация будет собрана для виртуальной машины с именем, которое определено в переменной vm_name.

4. Модуль vmware_guest_powerstate

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

О модуле vmware_guest_powerstate: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_powerstate_module.html.

а) Включение виртуальной машины:

- name: 'Start {{ vm_name }}'
  vmware_guest_powerstate:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    name: "{{ vm_name }}"
    state: powered-on
    validate_certs: no

б) Выключение виртуальной машины:

- name: 'Shutdown {{ vm_name }}'
  vmware_guest_powerstate:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    name: "{{ vm_name }}"
    state: shutdown-guest
    state_change_timeout: 200
    validate_certs: no

Или грубо:

- name: Set the state of a virtual machine to poweroff
  vmware_guest_powerstate:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    name: "{{ vm_name }}"
    state: powered-off
    validate_certs: no

в) Перезагрузка:

- name: 'Restart {{ vm_name }}'
  vmware_guest_powerstate:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    name: "{{ vm_name }}"
    state: reboot-guest
    state_change_timeout: 200
    validate_certs: no

Или грубо:

- name: Set the state of a virtual machine to restarted
  vmware_guest_powerstate:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    name: "{{ vm_name }}"
    state: restarted
    validate_certs: no

Виртуализация Proxmox

Рассмотрим немного примеров по работе с системой виртуализации proxmox.

Для работы с данной системой виртуализации есть в Ansible специализированный модуль proxmox_kvm. Его недостаток — относительно, небольшой перечень возможностей. Поэтому часть задачь будет решаться с помощью API.

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

  1. pve_host — имя сервера pve или его IP-адрес.
  2. pve_user — логин для подключения к vcenter.
  3. pve_password — пароль для подключения. Лучше хранить с использованием vailt.
  4. pve_node — имя ноды в датацентре.
  5. pve_port — номер порта, на котором слушает гипервизор (по умолчанию 8006).

Перейдем к примерам.

1. Сбор информации

В данном разделе мы рассмотрим примеры для получения информации с Proxmox.

а) Список виртуальных машин на хосте. Для получения информации о виртуальных машинах, которые находятся на хосте виртуализации нужно использовать API Proxmox. Для этого в ansible мы будем применять модуль URI:

- name: Get vms from pve
  uri:
    url: "https://pve.dmosk.local:8006/api2/json/cluster/resources?type=vm"
    headers:
      Authorization: PVEAPIToken=ansible@pve!Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7
    follow_redirects: all
  register: vm_list

* где:

  • pve.dmosk.local — адрес веб-интерфейса сервера виртуализации.
  • 8006 — порт для подключения в веб-интерфейсу.
  • ansible@pve — имя учетной записи, для которой создан токен доступа. Он создается в консоле управления Proxmox (Датацентр - Разрешения - API Tokens).
  • Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7 — имя токена и сам токен.

В результате мы сохраним список виртуальных машин в переменной vm_list.

б) Подробная информация о виртуальной машине или хосте виртуализации. Если мы хотим собрать побольше информации, нам также понадобится API:

- name: Get vm info
  uri:
    url: "https://pve.dmosk.local:8006/api2/json/<запрос>"
    headers:
      Authorization: PVEAPIToken=ansible@pve!Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7
    follow_redirects: all
  register: vm_info

Для получения различной информации о виртуальной машине или сервере мы используем различные запросы (отмечено как <запрос>). С полным списком того, что мы можем получить предлагаю ознакомиться на странице pve.proxmox.com/pve-docs/api-viewer.

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

nodes/<имя ноды>/qemu/<VMID>/config

Чтобы узнать hostname:

nodes/<имя ноды>/qemu/<VMID>/agent/get-host-name

И так далее.

в) Информация о машине с помощью модуля proxmox_kvm:

- name: "Get VMID"
  proxmox_kvm:
    node: "{{ pve_node }}"
    api_user: "{{ pve_user }}"
    api_password: "{{ pve_password }}"
    api_host: "{{ pve_host }}"
    name: "myvm"
    state: current
  register: vm_info

2. Операции с ВМ

Рассмотрим наиболее популярные действия над виртуальными машинами.

*) Остановить:

- name: "Останавливаем виртуальную машину Proxmox"
  proxmox_kvm:
    api_host    : "{{ pve_host }}"
    api_user    : "{{ pve_user }}"
    api_password: "{{ pve_password }}"
    node        : "{{ pve_node }}"
    name        : "myvm"
    state       : stopped
    force       : yes
    timeout     : 300

*) Удалить:

- name: "Удаляем виртуальную машину Proxmox"
  proxmox_kvm:
    api_host     : "{{ pve_host }}:{{ pve_port }}"
    api_user     : "{{ pve_user }}"
    api_password : "{{ pve_password }}"
    node         : "{{ pve_node }}"
    name         : "myvm"
    state        : absent

Работа с Docker

В данном разделе мы рассмотрим работу с контейнерами Docker.

Для корректной работы модуля, на целевом хосте должен быть установлен компонент python docker:

pip3 install docker

Обратите внимание, что должен использоваться python версии 3. Для версии 2 уже возникают ошибки при установке и работе модуля.

1. Работа с контейнером.

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

а) Создать новый контейнер можно с помощью сценария:

- name: Create a new container
  docker_container:
    name: new_container
    image: nginx

* в данном примере будет создан контейнер с именем new_container из образа nginx.

Создание контейнера с большим количиством опций:

- name: Create a new container
  docker_container:
    name: new_container
    image: nginx
    auto_remove: yes
    volumes:
      - "/tmp:/tmp"
    ports:
      - "80:80"

* где:

  • auto_remove — удалить контейнер после завершения его работы.
  • volumes — создаем volume. В данном случае, проброс каталога /tmp внутрь контейнера тоже в /tmp.
  • ports — на каком порту должна слушать хостовая система и на какой порт пробрасывать запросы внутрь контейнера.

б) Для удаления используем:

- name: Delete container
  docker_container:
    name: new_container
    state: absent

* важно указать имя контейнера и state со значением absent.

О docker_container: https://docs.ansible.com/ansible/2.9/modules/docker_container_module.html.

2. Запуск команды внутри контейнера.

Данное действие эквивалентно команде docker exec. В ansible можно выполнить с помощью модуля docker_container_exec или ранее рассмотренного docker_container. Рассмотрим оба варианта.

а) docker_container_exec.

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

ansible-galaxy collection install community.docker

Пример использования:

- name: Download wp-cli utility
  community.docker.docker_container_exec:
    container: "wordpress"
    command: curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
    chdir: /root

* в нашем примере мы скачаем утилиту wp-cli в контейнере wordpress. Команда будет запущена в директории chdir.

О docker_container_exec: https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_exec_module.html.

б) docker_container.

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

- name: Create a new container
  docker_container:
    name: new_container
    image: nginx
    command: whoami

Облачная платформа OpenStack

В данном разделе рассмотрим модуль работы с платформой виртуализации OpenStack. Данный модуль не входит в стандартный комплект и необходимо выполнить предварительные настройки для системы.

Устанавливаем коллекцию openstack.cloud на компьютер, где запускается ansible:

ansible-galaxy collection install openstack.cloud

Ознакомиться со всеми модулями данной коллекции можно на странице с документацией Ansible.

Также необходим python версии 3 с модулем openstacksdk. Установим данный модуль на компьютере, где будет запускаться сценарий:

python3 -m pip install --upgrade pip

python3 -m pip install openstacksdk

Для подключения и работы с OpenStack необходимы данные, характерные для хостинг-провайдера облачной услуги. Для этого изучите документацию по работе с OpenStack CLI на официальном сайте последнего. Также немного о работе с OpenStack CLI на моем сайте.

Для работы с модулями группы openstack нам нужно будет проходить авторизацию. Для этого мы будем использовать переменные, например:

    cloud:
      auth_url: https://infra.mail.ru:35357/v3/
      username: cloud-user-name
      password: cloud-user-password
      project_id: cloud-project-id
      user_domain_name: cloud-user-domain

* где:

  • auth_url — ссылка для подключения к облаку по openstack api.
  • username — учетная запись для прохождения аутентификации.
  • password — пароль от учетной записи.
  • project_id — идентификатор проекта.
  • user_domain_name — домен пользователей.

И так, рассмотрим простые примеры. Для удобства, попробуем разбить информацию по разделам.

Работа с виртуальными машинами

Используем модуль openstack.cloud.server.

Об openstack server: https://docs.ansible.com/ansible/latest/collections/openstack/cloud/server_module.html.

1. Создание инстанса:

- name: Создать виртуальную машину в облаке VK Cloud
  openstack.cloud.server:
    state: present
    auth:
      auth_url: "{{ cloud.auth_url }}"
      username: "{{ cloud.username }}"
      password: "{{ cloud.password }}"
      project_id: "{{ cloud.project_id }}"
      user_domain_name: "{{ cloud.user_domain_name }}"
    name: vm-name
    region_name: cloud-region-name
    image: cloud-image-id
    key_name: key-ssh-name
    timeout: 200
    flavor: cloud-vm-template
    availability_zone: MS1
    nics:
      - net-id: cloud-network-id
    security_groups:
      - default
      - ssh
    meta:
      hostname: my-cloud-vm-01

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

2. Удаление виртуальной машины:

- name: Удалить виртуальную машину в облаке openstack
  openstack.cloud.server:
    state: absent
    auth:
      auth_url: "{{ cloud.auth_url }}"
      username: "{{ cloud.username }}"
      password: "{{ cloud.password }}"
      project_id: "{{ cloud.project_id }}"
      user_domain_name: "{{ cloud.user_domain_name }}"
    name: vm-name

3. Используем cloud-config. Большинство облачных провайдеров используют образы операционных систем с установленным cloud-init. Данный компонент позволяет автоматизировать некоторые операции по администрированию системы. Например, мы можем при создании виртуальной машины выполнить дополнительные настройки:

- name: Создать виртуальную машину в облаке VK Cloud + выполнить предварительные настройки
  openstack.cloud.server:
    state: present
    ...
    config_drive: true
    userdata: |
      #cloud-config
      users:
        - name: dmosk
          groups: [ sudo ]
          shell: /bin/bash
          lock_passwd: false
          passwd: "$6$ssBWwe7wxa.lPToS$P5gm0y13m3X8VsK.czScN0ilTxYrB86KNhv7YvD4.4f0CdTDvfAV3W95pYQCPUdF2iRtIXBSVIw/ZYnxtBvOA."
      ssh_pwauth: true
      hostname: "my-cloud-vm-01"

* в данном примере нужно обратить внимание на следующие моменты:

  • config_drive — по умолчанию, cloud-init берет информацию для настройки с сервера. Данный параметр указывает, что должен использоваться пользовательский конфиг.
  • users — создаем пользователей в системе.
    • Некоторые провайдеры не позволяют создавать учетные записи с логином admin.
    • Хэш для passwd можно получить командой mkpasswd -m sha-512.
  • ssh_pwauth — по умолчанию на сервер нельзя зайти по ssh с использованием парольной аутентификации. Данная опция контролирует это поведение.

Получение информации

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

  • server_info — позволит получить информацию о виртуальной машине.
  • resources — сведения о различных ресурсах в облаке.
  • image_info — информация об образах для виртуальных дисков. 

Рассмотрим следующие примеры.

1. Получение информации о виртуальной машине. Выполняем с помощью модуля server_info:

- name: Получаем информацию о виртуальной машине vm1
  openstack.cloud.server_info:
    auth:
      auth_url: "{{ cloud.auth_url }}"
      username: "{{ cloud.username }}"
      password: "{{ cloud.password }}"
      project_id: "{{ cloud.project_id }}"
      user_domain_name: "{{ cloud.user_domain_name }}"
    name: vm1
  register: openstack

- debug:
    var: openstack

2. Список виртуальных машин. Используем модуль resources:

- name: Получаем список виртуальных машин
  openstack.cloud.resources:
    auth:
      auth_url: "{{ cloud.auth_url }}"
      username: "{{ cloud.username }}"
      password: "{{ cloud.password }}"
      project_id: "{{ cloud.project_id }}"
      user_domain_name: "{{ cloud.user_domain_name }}"
    service: compute
    type: server
  register: vm_list

3. Список образов для виртуальных дисков. Используем image_info.

а) все образы:

- name: Получаем список образов
  openstack.cloud.image_info:
    auth:
      auth_url: "{{ cloud.auth_url }}"
      username: "{{ cloud.username }}"
      password: "{{ cloud.password }}"
      project_id: "{{ cloud.project_id }}"
      user_domain_name: "{{ cloud.user_domain_name }}"
  register: images_list

б) конкретный образ:

  - name: Получаем информацию об образе "Ubuntu-22.04-202208"
    openstack.cloud.image_info:
      auth:
        auth_url: "{{ cloud.auth_url }}"
        username: "{{ cloud.username }}"
        password: "{{ cloud.password }}"
        project_id: "{{ cloud.project_id }}"
        user_domain_name: "{{ cloud.user_domain_name }}"
      image: Ubuntu-22.04-202208
    register: image_info

Работа с базами данных

В данном блоке рассмотрим примеры работы с различными базами данных.

 MySQL /MariaDB

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

  • mysql_db — позволяет редактировать базу данных или создавать дамп.
  • mysql_query — создание запросов.
  • mysql_user — работа с ролями.

Полный перечень модулей для работы с MySQL /MariaDB можно посмотреть на странице docs.ansible.com/ansible/latest/collections/community/mysql.

Они не идут в комплекте к ansible и нам необходимо установить их командой:

ansible-galaxy collection install community.mysql

Теперь рассмотрим несколько примеров для работы с MySQL / MariaDB через ansible.

1. Резервное копирование.

Для создания дампа используем сценарий:

- name: Dump mysql databases
  community.mysql.mysql_db:
    state: dump
    name:
      - db1
      - db2
    target: /tmp/dump.sql

* в данном примере мы создадим 2 дампа из баз db1 и db2 и сохраним результат в файл /tmp/dump.sql.

Для восстановления из дампа:

- name: Restore mysql databases
  community.mysql.mysql_db:
    name: db1
    state: import
    target: /tmp/dump.sql

2. Создать базу:

- name: Создаем базу данных для хранения данных почтового сервера
  community.mysql.mysql_db:
    name: postfix
    state: present
    encoding: utf8
    collation: utf8_general_ci

* создадим базу данныз с названием postfix и кодировкой UTF-8.

3. Создаем пользователя:

- name: Создаем пользователя и даем ему права на базу postfix
  community.mysql.mysql_user:
    state: present
    name: postfix
    password: postfix123
    priv: 'postfix.*:ALL'

PostgreSQL

Рассмотрим разные примеры работы с СУБД PostgreSQL. Мы будем использовать следующие модули:

Обратите внимание, что в некоторых случаях понадобится установка модуля командой:

ansible-galaxy collection install <имя модуля>

Подробнее можно найти инструкцию по ссылке выше.

1. Резервное копирование:

- name: Create dump
  postgresql_db:
    name: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD
    state: dump
    target: '/tmp/dump.sql'

2. Восстановление из дампа:

Сценарий напоминает резервное копирование, за исключением опции state:

- name: Create dump
  postgresql_db:
    name: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD
    state: restore
    target: '/tmp/dump.sql'

3. Выполнение запроса:

Запрос в базу можно сделать с помощью модуля postgresql_query:

- name: Select query
  postgresql_query:
    query: "SELECT * FROM users"
    db: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD

Рассмотрим конкретный пример с сохранением результата в переменную:

- name: Select query
  postgresql_query:
    query: "SELECT * FROM users WHERE name = '%s' or family = '%s'"
    db: clients
    login_host: localhost
    port: 5432
    login_user: dbuser
    login_password: dbpassword
    positional_args:
      - Andrey
      - Ivanov
  register: myclients

- name: Set variable
  set_fact:
    my_clients: myclients.query_result

* что мы сделали:

  • подключились к серверу баз данных на локальном сервере под пользователем dbuser с паролем dbpassword.
  • сделали запрос к базе clients. Запрос должен получить список всех записей из таблицы users, где в поле name есть значение Andrey или в поле family — Ivanov. Обратите внимание, что мы использовали паттерны с применением positional_args.
  • результат работы задачи мы зафиксировали в переменной myclients.
  • создали переменную my_clients, куда занесли результыты выборки.

4. Конфигурирование PostgreSQL. С помощью модуля postgresql_set можно вносить изменения в конфигурацию СУБД на подобие команды ALTER SYSTEM.

Например:

- name: Включаем SSL в PostgreSQL
  postgresql_set:
    name: ssl
    value: 'on'
    login_unix_socket: ''
  become: yes
  become_user: postgres

* в данном примере мы разрешим SSL на PostgeSQL. Обратите внимание на директиву login_unix_socket — она указывает на папку, где находится сокет подключения к СУБД. Его использует встроенная учетная запись postgres, чтобы подключаться к базе. По умолчанию, ansible ищет сокетный файл .s.PGSQL.5432 в каталоге /var/run/postgresql, но в некоторых случаях, например, при работе с Postgres Pro, данный путь может отличаться. У меня он был /tmp — в таком случае путь нужно указать явно, в противном случае мы получим ошибку unable to connect to database: connection to server on socket \"/var/run/postgresql/.s.PGSQL.5432\" failed: No such file or directory\n\tIs the server running locally and accepting connections on that socket?\n.

Запуск плейбука

Чтобы запустить плейбук, используем команду ansible-playbook.

Рассмотрим отдельно некоторые возможности при ее запуске.

1. Запуск плейбука с указанием файла инвентаризации:

ansible-playbook -i ./inventory.yml ./playbook.yml

* это простая команда запустит плейбук playbook.yml, с указанием необходимости использовать файл инвентаризации inventory.yml.

2. Начинать выполнение с определенной задачи.

При выполнении отладки, полезно запустить плейбук, но начать выполнение с определенной задачи. Остальные пропустить. 

Это можно сделать с помощью опции --start-at-task:

ansible-playbook ... --start-at-task="Start Job"

* в данном примере плейбук начнет выполнять задания с задачи Start Job.

3. Передача переменных.

Мы можем определить переменные при запуске плейбука с помощью опции --extra-vars. Возможна передача одной или нескольких переменных:

ansible-playbook ... --extra-vars "{ 'domain_name':'dmosk.local', 'ver':'5', 'vm_name':'test' }"

4. Использование тегов.

Теги позволяют пропускать или выполнять определенные задания. Предположим, у нас есть задание:

- name: Install postfix
  package:
    name: postfix
    state: present
  tags:
    - init postfix

* в данном примере мы навесили тег init postfix на задание Install postfix.

Теперь мы можем запустить плейбук:

ansible-playbook ... --tags "init postfix"

И задание будет выполнено. 

Или так:

ansible-playbook ... --skip-tags "init postfix"

И задание будет проигнорировано.

О тегах: https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html.

Обработка ошибок

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

1. Игнорировать ошибки.

Если ansible столкнется с ошибкой при выполнении задачи, работа плейбука будет завершена. Иногда, нужно пропустить ошибку при выполнении определенной задачи, чтобы выполнение было продолжено. Для этого существует опция ignore.

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

- name: Bad Task
  ...
  ignore_errors: yes 

б) чтобы игнорировать ошибки при подключении к хосту:

- name: Bad Task
  ...
  ignore_unreachable: yes 

в) также мы можем проигнорировать ошибку по условию, например:

- name: Выполняем shell
  shell: ...
  register: result
  failed_when: >
    result.rc != 0 and
    'already exists' not in result.stderr

* в данном примере мы выполняем какую-то каманду, и если она закончится с ошибкой, в сообщении которой есть фраза "already exists", ansible не воспримет это за ошибку и продолжит выполнение сценария.

2. Выбрасывать ошибку по своему условию.

Мы можем задать свое условие, когда Ansible должен считать, что задача отработана неправильно и нужно остановить работу. Это делается с помощью модуля fail.

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

- name: Fail if my data not loaded
  fail:
    msg: My data has not been loaded completely.
  when: my_dataload_status != "complete"

* в данном примере если переменная my_dataload_status не равна complete, системв вернет ошибку.

б) ошибка, если переменная содержит запрещенный символ:

- name: Fail if * in var
  fail:
    msg: "Ошибка — в переменной login есть символ *"
  when: '"*" in login'

О fail: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fail_module.html.

3. Действия в случае ошибки.

Рассмотрим 2 варианта:

  1. Мы, в любом случае, хотим выполнить чать задачь, даже если сценарий прекратит работу из-за ошибки.
  2. Мы хотим выполнить определенное действие, если сценарий прекратит работу из-за ошибки.

Это возможно реализовать с помощью block + rescue/always.

О blocks: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html.

Рассмотрим пример.

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

- name: Основной сценарий
  block:
    ...
  rescue:
    - name: Выполнить, если произошла ошибка
      debug:
        msg: 'Show error'

б) Выполнять задачу после always независимо от результата выполнения основного сценария:

- name: Основной сценарий
  block:
    ...
  always:
    - name: Выполнить в любом случае
      command: rm -f /tmp/test

в) И то и другое:

- name: Основной сценарий
  block:
    ...
  rescue:
    - name: Выполнить, если произошла ошибка
      debug:
        msg: 'Show error'
  always:
    - name: Выполнить в любом случае
      command: rm -f /tmp/test

* в данном примере мы отобразим текст Show error только при наличии ошибки, а также выполним команду rm -f /tmp/test независимо от исхода работы сценария.

Работа с выводом и строками

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

Конвертация типов данных

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

  • Строка — набор символов. Заключаются в кавычки.
  • Число — набор цифр. Записываются без кавычек.
  • Список (list) — массив данных. В качестве ключа используется пронумерованный индекс. Записываются в квадратных скобках.
  • Словарь (dict) — массив данных. В качестве ключа используется символный индекс. Записываются в фигурных скобках.
  • JSON — строка, имеющая формат записи в виде словаря. Считается удобным для передачи данных между системами.
  • YAML — строка, в которой данные разделяются переносами, а вложенность определяется отступами. Формат строго зависит от последних — лишний пробел нарушает его обработку. Удобно использовать для наглядного вывода информации.

Рассмотрим примеры преобразований данных из одного типа в другой.

1. Строку в число. Преобразуем с помощью int:

"100" | int

2. Список в json. Можно выполнить с помощью to_json:

list_var | to_json

или в json, который будет удобно читать:

list_var | to_nice_json

Преобразование строк

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

1. Получение массива с помощью split.

Метод split позволяет разбить строку на части по определенному символу. Полученные части станут значениями массива:

- name: Set variable
  set_fact:
    newvar: "{{ results.split(':') }}"

* в данном примере мы разобьем строку из переменной results на части по двоеточию и сохраним все это в переменную newvar.

2. Замены.

Мы рассмотрим несколько способов.

а) C помощью replace.

Позволяет в нашем выводе заменить одни символы другими:

- name: Set variable
  set_fact:
    newvar: "{{ results | replace(' ', '') }}"

* тут в переменную newvar будет записана строка results, из которой мы уберем все пробелы.

б) с помощью regex_replace.

Если нам нужно использовать регулярные выражения, то используем regex_replace. Например:

- name: Set variable
  set_fact:
    newvar: "{{ results | regex_replace('^test', '') }}"

... позволит искать слово test в начале строки.

3. Использование фильтров.

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

- set_fact:
    my_var: "{{ 'my-chars-12' | regex_search('\\d+') }}"

В данном примере мы создадим переменную my_var, значением которой будет чисо 12 (из строки my-chars-12 мы получим число).

О regex_search: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/regex_search_filter.html.

Работа с массивами

В данном подразделе будем работать со списками и словарями.

1. Объединение.

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

vars:
  list1:
    - name: name1
      value1: value11
    - name: name2
      value1: value12
  list2:
    - name: name1
      value2: value21
    - name: name3
      value2: value22

Мы можем их объединить по одному из полей, например, name:

- debug:
        msg: "{{ list1 | community.general.lists_mergeby(list2, 'name') }}"

* для lists_mergeby обязательно полное написание — community.general.lists_mergeby.

В результате, мы получим такой массив:

{
  name: name1
  value1: value11
  value2: value21
},
{
  name: name2
  value1: value12
},
{
  name: name3
  value2: value22
}

* как видим, два массива слились в один. Для ключа name1 также добавлено поле value2.

О lists_mergeby: https://docs.ansible.com/ansible/devel/collections/community/general/lists_mergeby_filter.html.

2. Преобразование элемента списка в словарь.

Предположим, у нас есть:

vars:
  list:
    - key: key1
      value: value1
    - key: key2
      value: value2
    - key: key3
      value: value3

Мы можем его преобразовать в словарь по имеющимся ключам (key).

а) Если имя ключа для ключа key, а имя ключа для значения value:

- debug:
    msg: "{{ list | items2dict }}"

Мы должны увидеть что-то на подобие:

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

б) Если имя ключа для ключа НЕ key, а имя ключа для значения НЕ value:

- debug:
    msg: "{{ list | items2dict(key_name='name', value_name='model') }}"

* в данном примере предполагается, что у нас есть массив с ключами name и model. Мы хотим, чтобы в качестве ключа для создаваемого словаря использовался первый, а для значения второй.

О items2dict: https://docs.ansible.com/ansible/devel/collections/ansible/builtin/items2dict_filter.html.

Формат JSON

Посмотрим, что мы можем сделать с json.

1. Фильтр данных. С помощью json_query мы можем выбирать только те данные, которые соответствуют критерию поиска.

а) один критерий:

- name: Display json filtered data
  debug:
    var: item
  loop: "{{ my_json | from_json | json_query(json_query_string) }}"
  vars:
    json_query_string: >-
      [?state=='running']

* в данном примере мы сделаем выборку всех данных, где значение поля state равно running.

б) два критерия:

- name: Display json filtered data
  debug:
    var: item
  loop: "{{ my_json | from_json | json_query(json_query_string) }}"
  vars:
    json_query_string: >-
      [?state=='running' && code==`200`]

* в данном примере мы добавили критерий code==`200`. Обратите внимание, что в нем мы используем другой тип кавычек. Это сделано не просто так — для строк используются кавычки " или ', для цифр — `.

в) фильтр с выбором конкретных полей:

- name: Display json filtered data
  debug:
    var: item
  loop: "{{ my_json | from_json | json_query(json_query_string) }}"
  vars:
    json_query_string: >-
      [?state=='running' && code==`200`].{key1: key1, key2: key2, key3: key3}

* в этои примере мы взяли ранее использовавшейся фильр и перечислили конкретные поля, которые мы хотим показать при выводе информации.

Шаблоны Jinja2

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

1. Перебор массива.

Предположим, нам нужно перебрать все элементы массива в шаблоне. Это можно сделать конструкцией:

{% for host in my_hosts %}
server "{{ host }}"
{% endfor %}

* в данном примере мы сделаем перебор по переменной my_hosts. Для каждого элемента массива будет создана строка со значением server <значение переменной>.

2. Задаем значение переменной с IF.

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

vars:
    var1: "test{% if hostname %}-value{% endif %}"
    var2: "{% if hostname %}value1{% else %}value2{% endif %}"
    var3: "{% if hostname == 'myhost' %}value1{% else %}value2{% endif %}"
    var4: "{{'value1' if (hostname) else 'value2'}}"

* мы рассмотрели следующие варианты:

  • var1 — если есть переменная hostname, то добавляем к test еще и -value. В итоге, получится test-value.
  • var2 — если есть переменная hostname, значение будет value1, в противном случае — value2.
  • var3 — если переменная hostname равна myhost, задаем для var3 значение value1, в противном случае — value2.
  • var4 — другой формат записи. Если есть переменная hostname, значение будет value1, в противном случае — value2.

Разное

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

1. Шифрование строки.

С помощью ansible-vault мы можем шифровать файлы и папки. Это позволит нам хранить секреты не в открытом виде. Данные расшифровываются в момент выполнения задач.

Данной командой мы получаем шифрованную строку:

ansible-vault encrypt_string

Система запросит ввести дважды пароль и предложит ввести строку, которую нужно зашифровать. После мы должны нажать 2 раза Ctrl + D — мы получим строку, которая начинается с !Vault и различные символы. 

Для того, чтобы в момент выполнения задачи ansible расшифровал данные, при запуске плейбука мы должны указать ключ --ask-vault-pass:

ansible-playbook ... --ask-vault-pass

Система потребует ввести пароль в консоль.

Также мы можем использовать файл с паролем. Тогда плейбук мы должны запустить с опцией vault-password-file:

ansible-playbook ... --vault-password-file <путь до файла с паролем>

или использовать системную переменную ANSIBLE_VAULT_PASSWORD_FILE:

export ANSIBLE_VAULT_PASSWORD_FILE=/root/.ansible-vault-password

Для дешифровки строки можно создать файл, в который добавить строку типа:

$ANSIBLE_VAULT;1.1;AES256
...

Допустим, создан файл с названием decrypt.txt. Тогда для дешифровки вводим команду:

cat decrypt.txt | ansible-vault decrypt

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

Об ansible-vault: https://docs.ansible.com/ansible/latest/user_guide/vault.html.

2. Завершить выполнение плейбука после определенной задачи.

С помощью данной конструкции:

- name: Stop Play
  meta: end_play

Мы можем полностью остановить выполнение задач для хоста:

- name: Stop Play for Host
  meta: end_host

О meta: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/meta_module.html.

3. Зависимые роли.

С помощью файла meta/main.yml в роли мы можем определить пред-роль, от которой зависит выполнение текущей роли. Для этого настраивается опция dependencies:

dependencies:
  - role: pred

4. Вставка роли и ее задач.

Позволяет в процессе выполнения задачи подключить роль. Делается при помощи include_role:

- name: "Include Other Role"
  include_role:
    name: other_role

А это пример, как подключить роль и сделать так, чтобы все ее задачи выполнились на определенном хосте:

- name: "Include Other Role"
  include_role:
    name: other_role
    apply:
      delegate_to: "{{ deploy_vm.instance.ipv4 }}"

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

- name: "Include Other Role From new_roles File"
  include_role:
    name: other_role
    tasks_from: new_roles

* в данном примере будут использовать файл не main.yml, а new_roles.yml.

В рамках одной и той же роли можно использовать include_tasks:

- name: Загружаем задачи из файла, который находится в той же роле
  include_tasks: tasks2.yml

5. Повторы и циклы при выполнении задач.

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

а) Повторный запуск задачи.

Выполняется с помощью retries (количиство повторов) и delay (задержка в секундах). Например, можно еще раз запустить задачу при возникновении ошибки:

- name: Run anything command
  command: /foo/bar/cmd
  register: result
  retries: 3
  delay: 60
  until: result is not failed

* в данном примере мы будем выполнять команду /foo/bar/cmd пока ее выполнение не закончится без ошибок. Количество повторов будет равен 3 с интервалом в 60 секунд.

Небольшой пример на странице https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#retrying-a-task-until-a-condition-is-met.

б) Использование циклов.

Выше в примерах мы часто упоминали loop для цикличного запуска задач в разными вариантами выполнения, например:

- name: Create Directories with loop
  file:
    path: "{{ item }}"
    state: directory
  loop:
    - '/var/log/prometheus'
    - '/var/log/grafana'

* в данном примере задача будет выполнена два раза — для создания каталогов /var/log/prometheus и /var/log/grafana.

в) Циклы из списков:

- name: Задаем настройки для nginx
  replace:
    path: /etc/nginx/nginx.conf
    regexp: "^{{ item.key }}=(.*)"
    replace: '{{ item.key }}={{ item.value }}'
  with_dict: "{{ nginxcfg }}"

* подразумевается, что у нас есть переменная nginxcfg в виде списка (массива). В данном примере ansible пробежит по каждой записи данного списка и сделает соответствующую замену в конфигурационном файле nginx.conf.

г) Повтор задачи несколько раз.

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

vars:
  var_count: 5

...
loop: "{{ range(1, (1 | int+var_count)) | list }}"
loop_control:
  loop_var: var_count_item

* в данном примере мы создаем цикл от 1 до 5 (значение переменной var_count). Чтобы ключ для цикла item не конфликтовал с ключами других циклов, которые могут использоваться внутри задачи, меняем имя item (по умолчанию) на var_count_item с помощью директивы loop_var.

О loop: https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html.

6. Объединение задач в блоки.

Это позволит установить общие свойства и условие для нескольких задач. Такая форма записи уменьшит количиство строк и упростит восприятие.

Синтаксис записи:

- name: Block Name
  block:
     - name: Task 1
       ...

     - name: Task 2
       ...

     - name: Task 3
       ...
  when: ansible_facts['distribution'] == 'CentOS'
  become: true
  become_user: root
  ignore_errors: yes

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

О block: https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html.

7. Обращения к DNS.

При помощи модуля dig мы можем отправлять запросы на серверы имен, получая сведения о доменных записях. Сценарии использования могут быть различные.

а) простой запрос на получение IP-адреса для А-записи:

- name: NSLOOKUP for domain dmosk.ru
  debug:
    msg: "{{ lookup('dig', 'www.dmosk.ru.')}}"

* в данном примере мы получим адрес для узла www.dmosk.ru.

б) запрос на получение IP-адреса для А-записи через определенный DNS-сервер:

- name: NSLOOKUP for domain dmosk.ru over 8.8.8.8
  debug:
    msg: "{{ lookup('dig', 'www.dmosk.ru.', '@8.8.8.8')}}"

О dig: https://docs.ansible.com/ansible/latest/collections/community/general/dig_lookup.html.

8. Отправка POST запроса.

Ранее мы рассматривали модуль uri для скачивания файла. Однако, данный модуль может быть более полезным. Например, мы можем отправлять запросы к API:

- name: Send Post Request
  uri:
    url: https://api.dmosk.ru/api/v2/send_request
    method: POST
    body_format: form-urlencoded
    return_content: false
    body:
      id: 1
      name: Test
      action: Read
    headers:
      Authorization: Bearer 577f573d09a1949436e3a07f7e9de6c5

* в данном примере будет отправлен POST-запрос на адрес https://api.dmosk.ru/api/v2/send_request. В POST-данных мы отправим idname и action. Также дополнительно мы отправили заголовок Authorization.

Об uri: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html.

9. Дата и время.

Мы можем использовать встроенную переменную ansible_date_time для отображения даты и времени в разных форматах. Чтобы переменная не была пустой, ansible должен собрать факты о хосте. Для этого используем инструкцию gather_facts: yes.

Чтобы увидеть все форматы, напишем такой сценарий:

- name: Ansible fact - ansible_date_time
  debug:
   var: ansible_date_time

Мы должны увидеть что-то на подобие:

ok: [localhost] => {
    "ansible_date_time": {
        "date": "2022-11-11",
        "day": "11",
        "epoch": "1668182701",
        "hour": "19",
        "iso8601": "2022-11-11T16:05:01Z",
        "iso8601_basic": "20221111T190501847269",
        "iso8601_basic_short": "20221111T190501",
        "iso8601_micro": "2022-11-11T16:05:01.847269Z",
        "minute": "05",
        "month": "11",
        "second": "01",
        "time": "19:05:01",
        "tz": "MSK",
        "tz_offset": "+0300",
        "weekday": "Пятница",
        "weekday_number": "5",
        "weeknumber": "45",
        "year": "2022"
    }
}

Чтобы получить только дату, используем:

- name: Get date
  debug:
   var: ansible_date_time.date

10. Генерация случайных строк

Очень полезно, если нам нужно сгенерировать пароль. Выполняется с помощью модулей password и lookup.

Синтаксис следующий:

"{{ lookup('password', '<путь до файла, куда сохраняем строку> chars=<через запятую перечисляем разрешенные символы> length=<длина строки>') }}"

Пример:

- name: Generate new password
  set_fact:
    password_var: "{{ lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=2') + lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits,{{ spec_chars }} length=8') + lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=2') | replace('l', 'L') | replace('1', '2') | replace('O', 'o') | replace('I', 'i') }}"
  vars:
    spec_chars: '!@#$%^&()_-<>{}'

- debug:
    var: password_var

* в данном примере мы получим строку из 12-и символов:

  • первые 2 и последние 2 символа не будут содержать спецсимволов. Я не люблю специальные символы на концах.
  • специальные символы перечислены в переменной spec_chars. Намеренно, исключены некоторые из них, которые могут вызывать проблемы, например звездочка (*).
  • после получения строки мы заменим некоторые символы, которые могу вызывать путаницу. Например, большая буква O похожа на ноль, большая I (и) похожа на маленькую l (эль). И так далее.
  • Результат будет записан в переменную password_var. С помощью модуля debug мы отобразим полученную строку на экране.

О password: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/password_lookup.html.

11. Добавить хост в инвентаризацию.

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

- name: Добавим хост mail в группу 'servers' с переменной foo
  add_host:
    name: mail
    ansible_ssh_host: 192.168.0.11
    ansible_ssh_port: 22
    groups: servers
    foo: bar

О add_host: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/add_host_module.html.

12. Выполнение действия после отработки задачи.

Предположим, что нам нужно перезапустить сервис, но только в случае, если меняется файл конфигурации. В Ansible предусмотрена опция notify, которая указывает, какую задачу нужно выполнить в случае, если действие привело к измененному состоянию. Например:

- name: Установка расширений PHP
  package:
    name: "{{ item }}"
    state: present
  loop:
    - php-mysql 
    - php-mbstring 
    - php-imap
  notify: restart apache

* в данном примере будет запущена задача restart apache, но только в случае, если на систему будет установлен один из перечисленных пакетов. Если пакеты не пришлось поставить (они уже были установлены), то и notify не отработает.

Важно отметить, что задачи notify необходимо создавать в отдельной секции handlers. Либо в самом файле с заданиями:

tasks:
  ...

handlers:
  ...

Либо в отдельной директории роли — каталоге handlers.

13. Использование бастиона для подключения к хосту по SSH.

У нас может возникнуть необходимость подключиться к серверу, который недоступен по сети напрямую, но к которому можно подключиться через другой компьютер, к которому есть доступ по SSH. Такие узлы принято называть Jump Host или Бастионы.

Для подключения через другой хост необходимо добавить переменную ansible_ssh_common_args со значением '-o ProxyCommand="ssh -p <port> -W %h:%p -q <username>@<bastion.host.net>"'. Удобнее всего это сделать в инвентарном файле, например:

---

all:
  hosts:
    my_server:
      ansible_ssh_host: 1.2.3.4
      ansible_ssh_port: 22
      ansible_ssh_common_args: '-o ProxyCommand="ssh -p 22 -W %h:%p -q root@5.6.7.8"'

* в данном примере мы выполним подключение к хосту 1.2.3.4 через компьютер 5.6.7.8.

Читайте также

Другая информация по Ansible:

1. Инструкция по установке и запуску Ansible на Linux.

2. Примеры ролей Ansible для установки сервисов и настройки системы.

# DevOps # Управление
Дмитрий Моск — частный мастер
Была ли полезна вам эта инструкция?

Да            Нет