Содержание

Ansible

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

Пособие по Ansible
Документация
Список модулей

Task Базовый блок. Обычно модуль с параметрами, выполняющий определённое действие.
Role Типа функции - объединяет задачи, файлы, шаблоны, переменные и обработчики в единое целое.
Play Сущность, связывающая задачи и роли с целевыми серверами.
Playbook Файл yaml для запуска, где перечислены все нужные плеи.

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

# копирование файла, используя модуль copy
ansible localhost -m copy -a "src=file.txt dest=/tmp/file.txt"

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

Роли в Ansible - это список задач.
Плейбук - это список плеев, где перечисляются роли и к каким серверам они применяются.

Задача

Базовый элемент.

# Cписок из 5 задач
 
- name: Модуль maven_artifact скачивает приложение из Nexus
  maven_artifact:
    dest: "/opt/project/backend/lib/file.jar"
    repository_url: "https://nexus-srv/repository/project"
    group_id: "com.group.id"
    artifact_id: "backend"
    version: "0.1.0"
    
- name: Добавление сервисного пользователя
  user:
    name: "serviceuser"
    create_home: no
    shell: /sbin/nologin
  
- name: Шаблонизация конфигурации - управление настройками приложения с помощью переменных
  template:
    src: project-backend.service.j2
    dest: /etc/systemd/system/project-backend.service

- name: Перечитать конфигурацию systemd
  systemd:
    daemon_reload: yes

- name: Запуск сервиса
  service:
    name: project-backend
    state: running

name отображается в отчёте. После него идёт название модуля. Справка по модулю - ansible-doc <имя модуля>.

Управление ошибками / кодами возврата, условия

https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html

fail / assert

Проверка, всё ли выполнилось так, как нужно.
fail - инициировать сбой
assert - убедиться, что условие выполняется (в некотором смысле, fail наоборот)

- hosts: 127.0.0.1
  gather_facts: no
  connection: local
  
  vars:
    fail_via_fail: false
    fail_via_assert: false
    fail_via_complex_assert: true
    
  tasks:
    - name: Fail if conditions warrant a failure
      fail:
        msg: "Task failed"
      when: fail_via_fail
      
    - name: Fail if an assertion isn't validated
      assert:
        that: "fail_via_assert != true"
      
    - name: Assertions can have certain conditions
      assert:
        that:
          - "fail_via_fail != true"
          - "fail_via_assert != true"
          - "fail_via_complex_assert != true"

Debug

Вывод информации.

tasks:
# Вывод переменной foo, свойства rc
  - debug: var=foo.rc
# Лучше использовать такой синтаксис, т. к. если в свойстве будет дефис, то может быть ошибка.
  - debug: var=foo['rc']
 
# Вывод сообщения
  - debug: msg="Hello {{ foo['rc'] }}"

Handlers

Это дополнительные задачи, обработчики, вызываемые по запросу. Вызываются из задачи с помощью notify: <имя хэндлера>, если только эта задача реально выполняется, т. е. что-то изменяется.
Хэндлеры выполняются в конце плейбука после всех задач, если нет спец. настроек.

playbook_apache.yml
- name: Install Apache
  hosts: staging_servers
  become: yes

  vars:
    source: ~/for_site/index.html
    destination: /var/www/html

  tasks:
  - name: Install
    package:
      name: apache2
      state: latest
  - name: Start & enable
    service:
      name: apache2
      state: started
      enabled: yes
  - name: Copy index.html
    copy:
      src: "{{ source }}"
      dest: "{{ destination }}"
      mode: 0774
      owner: www-data
      group: www-data
    notify: restart apache

  handlers:
    - name: restart apache
      service:
        name: apache2
        state: restarted

Если нужно выполнить хэндлер сразу после какой-то задачи, не дожидаясь выполнения всех остальных задач, то сразу после неё ставится спецзадача

- name: Kick handlers immediately
  meta: flush_handlers

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

ansible-playbook -i hosts playbook.yml --force-handlers

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

    notify:
      - restart apache
      - restart memcached

А можно вызывать обработчик из другого обработчика, т. е., вызвать restart memcached из restart apache.

Роль

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

Файловая структура роли:

defaults/main.yml # значения переменных по умолчанию для роли.
files/ # каталог с файлами, не требующими шаблонизации.
handlers/main.yml # обработчики, запускаются только при получении соответствующих уведомлений (notify) от задач.
meta/main.yml # метаданные с описанием авторов роли, зависимостей и версии.
tasks/main.yml # задачи, запускаемые ролью.
templates/ # каталог с jinja2-шаблонами.
tests/
vars/main.yml # переменные для роли.

Генерация файловой структуры роли: ansible-galaxy init <role name>.

Include / Import

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

handlers:
  - import_tasks: handlers/apache.yml
  - import_tasks: handlers/app.yml
tasks:
  - import_tasks: tasks/apache.yml
  - import_tasks: tasks/app.yml

Import отличается от include тем, что import сразу формирует файл плейбука при парсинге во время запуска с подстановкой переменных, а include выполняет эти файлы, когда доходит до них. Т. е., если в задаче используются какие-то переменные, значение которых формируется по ходу выполнения плейбука, то нужно использовать Include. Если это просто для выноса кусков кода для расхламления плейбука, то тогда Import.

:!: После слов import и include можно писать что угодно: import_aaa или include_greatThings

Импортировать/включать можно и целый плейбук. Для этого include нужно разместить на верхнем уровне.

name: playbook1
hosts: all
become:true

tasks:
  - import: tasks/apache.yml

- include: playbook2.yml

Плейбук

Связь задач/ролей с целевыми серверами.

playbook.yaml
---
- name: Запуск backend сервиса project
  # Шаблон целевых хостов это группа хостов с именем backend
  hosts: backend
  # Список ansible-ролей для backend-серверов
  roles:
    - project-backend

- name: Запуск frontend сервиса project
  # Шаблон целевых хостов это группа хостов с именем frontend
  hosts: frontend
  # Список ansible-ролей для frontend-серверов
  roles:
    - project-frontend

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

Тэги/метки

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

  tasks:
  - name: ...
    shell: ...
    tags:
      - api
      - echo

Запустит только задачи с тэгом api.

ansible-playbook playbook.yml --tags=api

Inventory

Группы хостов берутся из inventory. Это в простом случае может быть текстовый файл со списком IP-адресов или имён серверов, а может быть и в формате yaml.

# Названия чувствительны к регистру - [staging_servers] и [staging_Servers] будут разными группами.
 
# Все сервера из всех групп входят в группу all
# можно писать сервера вообще без группы, они будут входить в группу ungrouped
192.168.1.1
192.168.1.2
server.example.com
server2.example.com ansible_host=192.168.1.3
 
[staging_db]
192.168.1.51
192.168.1.52
[staging_web]
192.168.1.61
192.168.1.62
[staging_app]
192.168.1.71
192.168.1.72
 
# Группа staging_all, объединяющая группы staging_db, staging_db и staging_app
[staging_all:children]
staging_db
staging_db
staging_app
 
[staging_servers]
# чтобы не прописывать одинаковые значения ansible_user, ansible_ssh_private_key и т. д., можно прописать переменные
# См. [staging_servers:vars] ниже
#k2      ansible_host=192.168.1.22       ansible_user=user       ansible_ssh_private_key=/home/user/.ssh/id_ed25519
#k3      ansible_host=192.168.1.23       ansible_user=user       ansible_ssh_private_key=/home/user/.ssh/id_ed25519
k2      ansible_host=192.168.1.22
k3      ansible_host=192.168.1.23
 
# Vars лучше не прописывать в файле inventory, а держать отдельно, но так тоже можно
[staging_servers:vars]
ansible_user=user
ansible_ssh_private_key=/home/user/.ssh/id_ed25519
 
[windows_servers]
windows2012	ansible_host=192.168.1.10
windows2016	ansible_host=192.168.1.11
 
[windows_servers:vars]
ansible_user				= myadmin
ansible_port				= 5986
ansible_connection			= winrm
ansible_winrm_server_cert_validation	= ignore
# Показать список хостов с принадлежащими им переменными
ansible-inventory --list
# Типа tree для файлов
ansible-inventory --graph
 
# проверка связи (inventory - hosts.txt, все хосты, модуль ping)
ansible -i hosts.txt all -m ping

Проблема в том, что если на сервера раньше не заходили, запросит кучу подтверждений ssh fingerprint. Чтобы этого избежать, рисуем ansible.cfg, где заодно указываем inventory-файл по умолчанию.

ansible.cfg
[defaults]
host_key_checking	= false
inventory		= ./hosts.txt
# проверка связи c учётом ansible.cfg
ansible all -m ping

Формат yaml:

all: # Обязательный параметр (все сервера)
  children:
    backend: # (группа серверов backend)
      hosts:
        192.168.3.4:
        dev-backend.example.com:
    frontend:
      hosts:
        192.168.3.5:
        dev-frontend.example.com:
    dev:
      hosts:
        dev-backend.example.com:
        dev-frontend.example.com:
    prod:
      hosts:
        192.168.3.4:
        192.168.3.5:  

После этого можно запускать плейбук с ограничением по группе, например, ansible-playbook playbook.yaml --limit dev

В файле inventory можно прописывать переменные, но это не рекомендуется:

all:
  children:
    backend:
      hosts:
        dev-backend.example.com:
          # Переменная только для этого хоста
          ansible_user: ansible
      vars: # Переменные для группы backend
        backend_version: 3.1.1
  vars: # Переменные для всех хостов в inventory
    ansible_connection: ssh

Лучше создавать файлы с переменными в каталоге group_vars рядом с playbook.yaml, например, файл group_vars/all.yaml будет выглядеть так:

ansible_connection: ssh

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

Переменные

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

Переменные могут быть определены в плейбуках, в inventory, внутри ролей или передаваться в командной строке запуска плейбука (т. н. extra vars).
:!: В именах переменных могут использоваться только буквы, цифры (не первый символ) и подчёркивания.

# Extra vars имеют наивысший приоритет
ansible-playbook playbook.yaml --extra-vars "my_var=value"

Переменные, указанные в файлах group_vars/*.yaml, в свою очередь, имеют приоритет над переменными, заданными в Ansible-ролях.

https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#ansible-variable-precedence
https://docs.ansible.com/ansible/latest/reference_appendices/general_precedence.html#general-precedence-rules

Добавить переменную в ~/.bash_profile.
Если строки с regexp: нет, то она создастся. Если есть, то она изменится.
Если под regexp: попадает несколько строк, то изменится последняя.

- name: Var playbook
  hosts: k3
  
  tasks:
  - name: Add environment variable to the remote user's shell
    lineinfile:
      dest: "~/.bash_profile"
      regexp: '^ENV_VAR='
      line: 'ENV_VAR=value'
# Вывод переменной
  - name: Get the ENV_VAR value
    shell: 'source ~/.bash_profile && echo $ENV_VAR'
    # Если не указать bash, то может запуститься sh с ошибкой "stderr": "/bin/sh: 1: source: not found"
    args:
      executable: /bin/bash
    register: envvar
# Либо в составе сообщения, либо просто вывести переменную
  - debug: msg="The value is {{ envvar.stdout }}"
  - debug: var=envvar.stdout

:!: Общесистемные переменные нужно загонять в файл /etc/environment. Понадобится become: true.

Местоположение переменных

Положим, есть плейбук со скачиванием файла и настройкой прокси-сервера.

- name: Var playbook
  hosts: k3
  
  tasks:
  - name: Download a file
    get_url:
      url: http://ipv4.download.thinkbroadband.com/5MB.zip
      dest: /tmp
    environment:
      http_proxy: http://proxy.example.com
      https_proxy: https://proxy.example.com

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

  vars:
    proxy_vars:
      http_proxy: http://proxy.example.com
      https_proxy: https://proxy.example.com
    
  tasks:
  - name: ...
    environment: proxy_vars

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

  environment:
    http_proxy: http://proxy.example.com
    https_proxy: https://proxy.example.com
    
  tasks:
  - name: ...
  - name: ...

Переменные из файла:

  vars:
    key: value
  vars_files:
    - vars/main.yml

Задать переменную ansible_super_secret из значения переменной окружения SUPER_SECRET:

ansible_super_secret: "{{ lookup('env', 'SUPER_SECRET') }}"

Факты

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

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

# принудительный запуск на локальной машине
ansible localhost -m setup

Так как сбор фактов - операция ресурсоёмкая, применяются следующие техники для ускорения работы:

  1. Отключение - применяется в облаках, когда VM автоматически создана, но ОС внутри ещё не запустилась. Чтобы Ansible не выдал ошибку, даётся указание ждать подключения, а сбор данных выключить.
    - hosts: development
      gather_facts: no
      tasks:
        - name: waiting for connection
          wait_for_connect
  2. Фильтрация - собирать только необходимую информацию.
    - hosts: development
      gather_facts: no
      tasks:
        - name: wait for connection
          wait_for_connect
        - name: selected facts
          ansible.builtin.setup:
            filter:
              - 'ansible_distribution'
              - 'ansible_machine_id'
              - 'ansible_*_mb'
  3. Кэширование - настраивается в файле ansible.cfg.
    [inventory]
    cache = True
    cache_plugin = jsonfile
    cache_connection = ~/.cache/ansible
    cache_timeout = 3600

Шаблоны Jinja2

Это движок для подстановки переменных или для динамического формирования конфигурационных файлов. Ссылка на переменную идёт в любом файле yaml в двойных фигурных скобках.

- name: Install package {{ package_name }}
  package:
    name: "{{ package_name }}"

Чтобы создавать файлы на основе шаблонов, используется модуль template.

- name: Create app backend service unit 
  template:
    src: app-backend.service.j2
    dest: /etc/systemd/system/app-backend.service

Если переменные бэкенда описаны в group_vars/backend.yml:

backend_maven_version: 3.4.1
backend_service_user: app_user
backend_report_directory: /opt/app/reports
backend_lib_directory: /opt/app/lib/ 

То можно генерировать systemd-unit на основе шаблона app-backend.service.j2:

[Unit]
Description=app

[Service]
User={{ backend_service_user }}
Restart=always
Environment=REPORT_PATH={{ backend_report_directory }}
ExecStart=/usr/bin/java -Xrs -jar {{ backend_lib_directory }}/sausage-store-{{ backend_maven_version }}.jar

[Install]
WantedBy=multi-user.target 

Динамические переменные

Используются для нескольких ОС, где разные пути к конфигам и один и тот же пакет называется по-разному, например apache2 в Debian и httpd в CentOS. Создаётся несколько файлов с переменными:

apache_RedHat.yml
apache_package: httpd
apache_service: httpd
apache_config_dir: /etc/httpd/conf.d
apache_default.yml
apache_package: apache2
apache_service: apache2
apache_config_dir: /etc/apache2/sites-enabled

В плейбуке рисуется раздел с предварительным заданием (на уровне tasks), который читает первый совпадающий файл с переменными:

  pre_tasks:
#    - debug: var=ansible_os_family
    - name: Load variable file
      include_vars: "{{ item }}"
      with_first_found:
        - "vars/apache_{{ ansible_os_family }}.yml"
        - "vars/apache_default.yml"

Вместо ansible_os_family можно использовать ansible_distribution.

ansible.cfg

Базовый способ настройки Ansible. В ansible.cfg определены директивы конфигурации для всего набора утилит (ansible, ansible-playbook, ansible-galaxy, ansible-vault и т. д.). В этом файле указывается расположение ролей, конфигурация inventory и имя пользователя для подключения к хостам. Пример:

[defaults]
roles_path = roles
inventory = inventory

remote_user = ansible

vault_password_file = .vault
host_key_checking = False
[privilege_escalation]
become = true ; повышать привилегии (sudo)

Ansible поддерживает следующие способы передачи параметров конфигурации (по убыванию приоритета):

  1. Переменные (если значение переменной присвоено несколько раз — используется последнее)
  2. Значения из плейбуков
  3. Значения, установленные через аргументы командной строки
  4. Значения из файла конфигурации

ansible-config покажет список всех параметров конфигурации, их стандартные значения и пояснения, откуда взялось то или иное значение.

Ansible ищет конфигурационные файлы в следующем порядке:

  1. Путь из переменной окружения ANSIBLE_CONFIG
  2. ./ansible.cfg (файл в текущем каталоге)
  3. ~/.ansible.cfg (файл в домашнем каталоге)
  4. /etc/ansible/ansible.cfg

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

https://docs.ansible.com/ansible/devel/reference_appendices/config.html

Vault

Шифрованное хранилище для секретных данных - паролей, токенов и т. п. Командой ansible-vault их можно зашифровать, а при запуске Ansible передать спец. пароль, который расшифрует данные в процессе работы:

ansible-vault encrypt_string "VerySecretPassword!@#$%" --name secret_pass

Вывод команды указывается в group_vars/<my_group>.yml:

secret_pass: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  39836982658142836487543449583409549856387698734580384098152098347874095687623654
  ...
  8457

Передача пароля для расшифровки происходит в интерактивной сессии при запуске ansible-playbook с параметром --ask-vault-pass :

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

При запуске Ansible в CI-системе этот способ не подойдёт и потребуется механизм передачи пароля без участия пользователя.
Для этого нужно сформировать файл с паролем на предыдущем шаге CI и указать его расположение в ansible.cfg:

vault_password_file = .vault

Файл .vault необходимо добавить в .gitignore!

Структура проекта Ansible, коллекции

Пример структуры.

.
├── README.md
├── ansible.cfg
├── roles
│   ├── backend
│   └── frontend
├── group_vars
│   ├── all.yaml
│   ├── backend.yaml
│   └── frontend.yaml
├── inventory
│   ├── static.yaml
│   └── cloud.yaml
└── playbook.yaml

Коллекция - совокупность необходимых ролей, плейбуков, плагинов для inventory, собственных модулей. Коллекции можно хранить в git и устанавливать их оттуда или публиковать на специальном сервисе Ansible Galaxy.

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

  1. Описание зависимостей (список используемых коллекций) в особом файле, например, requirements.yaml
  2. Установка зависимостей утилитой ansible-galaxy и формирование inventory
  3. Запуск команды ansible-playbook с указанием плейбука

Этот рабочий процесс может быть запущен на CI системе или в специальной системе, построенной поверх Ansible — AWX. У AWX есть GUI, упрощающий некоторые задачи по работе с Ansible, например, сделать кнопку для техподдержки для выполнения рутинных операций, которые требуются как реакция на инцидент или запрос от пользователя.

Ansible Galaxy

Ansible Galaxy - общий ресурс для хранения коллекций и ролей.

По умолчанию роли оттуда ставятся в глобальный каталог типа /etc/ansible/roles. Если нужно установить локально в текущий плейбук, нужно в ansible.cfg прописать

roles_path = ./roles

Рядом положить файл requirements.yml с перечислением ролей-зависимостей и, по желанию, их версий.

---
roles:
  - name: elliotweiser.osx-command-line-tools
    version: 2.3.0
  - name: geerlingguy.mac
    version: 4.0.0

После этого запустить установку ролей из Galaxy, указав файл зависимостей. Скачанные роли окажутся в подкаталоге roles, как указано в файле конфигурации.

ansible-galaxy install -r requirements.yml

Плейбук:

---
- hosts: localhost
  connection: local
  
  vars:
    homebrew_installed_packages:
      - pv
  roles:
    - elliotweiser.osx-command-line-tools
    - role: geerlingguy.mac
      become: true

Проверка/тестирование

По степени увеличения сложности:

  1. yamllint
  2. ansible-playbook –syntax-check - встроенная проверка синтаксиса. Ловит не всё, например, include: не может, т. к. для этого нужно запустить плейбук.
  3. ansible-lint
  4. molecule test (integration)
  5. ansible-playbook –check (against prod)
  6. parallel infrastructure

yamllint

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

# Установка
sudo apt install yamllint
# или
pip3 install yamllint
 
yamllint playbook_delegate.yml
playbook_delegate.yml
  1:1       error    too many blank lines (1 > 0)  (empty-lines)
  2:1       warning  missing document start "---"  (document-start)
  4:11      warning  truthy value should be one of [false, true]  (truthy)
  11:1      warning  comment not indented like content  (comments-indentation)
  13:81     error    line too long (156 > 80 characters)  (line-length)
  18:13     warning  truthy value should be one of [false, true]  (truthy)
  19:81     error    line too long (155 > 80 characters)  (line-length)
  23:1      error    too many blank lines (3 > 0)  (empty-lines)

ansible-lint

Проверка на соответствие best practices.

# Установка
sudo apt install ansible-lint
# или
pip3 install ansible-lint
 
ansible-lint playbook2.yml
WARNING  Listing 4 violation(s) that are fatal
yaml: truthy value should be one of [false, true] (truthy)
playbook2.yml:3
 
package-latest: Package installs should not use latest
playbook2.yml:11 Task/Handler: Install
 
yaml: wrong indentation: expected 4 but found 2 (indentation)
playbook2.yml:11
 
yaml: truthy value should be one of [false, true] (truthy)
playbook2.yml:19
 
You can skip specific rules or tags by adding them to your configuration file:
# .ansible-lint
warn_list:  # or 'skip_list' to silence them completely
  - package-latest  # Package installs should not use latest
  - yaml  # Violations reported by yamllint
 
Finished with 4 failure(s), 0 warning(s) on 1 files.

Конкретные задачи

Копирование с одного удалённого сервера на другой

Существует модуль synchronize, где можно настроить делегирование и тем самым запустить копирование непосредственно с одного хоста на другой.

- name: Synchronization of src on delegate host to dest on the current inventory host.
  ansible.posix.synchronize:
    src: /first/absolute/path # Путь на делегируемом хосте-источнике delegate.host
    dest: /second/absolute/path # Путь на текущем обрабатываемом хосте
  delegate_to: delegate.host

Но есть проблема аутентификации: каким образом обеспечить доступ с делегируемого хоста на конечные? Есть вариант SSH Agent Forwarding, но это вроде как небезопасно. Поэтому в общем случае копируем сначала на менеджера (достаточно одного раза - run_once: true), а потом с него на конечные сервера.

---
- name: Sync
  hosts: k3
  gather_facts: no

  tasks:
  - name: Copy WAR to worker from source
    command: rsync k2:/home/user/file.war /home/user/file.war
    run_once: true
    delegate_to: localhost

  - name: Copy WAR to destination from worker
    synchronize:
      src: /home/user/file.war
      dest: /home/user/file.war

Останов, копирование .war-файла с эталонного сервера и конфигов, запуск, удаление паролей

- name: "Get service status"
  shell: systemctl --user is-active {{ service_name }}.service
  register: service_status
  ignore_errors: true

- name: "Get process ID"
  shell: pgrep -f "{{ catalina_base }}"
  register: process_id
  ignore_errors: true

- name: "Stop process by script"
  script: "{{ deploy_dir }}/stop.sh"
  args:
    creates: "~/.config/systemd/user/{{ service_name }}.service"
  when: service_status.stdout != 'active' and process_id.stdout != ""

- name: "Kill process"
  shell: kill -9 "{{ process_id.stdout }}"
  when: service_status.stdout != 'active' and process_id.stdout != ""

- name: "Stop service"
  systemd:
    scope: user
    name: "{{ service_name }}"
    state: stopped
  when: service_status.stdout == 'active'

- name: "Create folders"
  file:
    path: "{{ item }}"
    state: directory
  with_items:
    - "~/.config/systemd/user"
    - "{{ catalina_base }}/conf"
    - "{{ catalina_base }}/webapps"
    - "{{ pool_settings_dir }}"

- name: "Enable systemd lingering"
  shell: loginctl enable-linger $USER

- name: "Copy WAR file to worker from source ({{ war_source_server }})"
  command: rsync "{{ deploy_user }}@{{ war_source_server }}:{{ catalina_base }}/webapps/app.war" /tmp/app.war
  delegate_to: localhost
  run_once: true
  when: copy_war|bool == true

- name: "Copy WAR to destination"
  syncronize:
    src: /tmp/app.war
    dest: "{{ catalina_base }}/webapps/app.war"
  when: copy_war|bool == true

- name: "Copy conf files"
  copy:
    src: "{{ item }}"
    dest: "{{ catalina_base }}/conf"
    owner: "{{ deploy_user }}"
    mode: 0660
  with_fileglob:
    - "conf/*"
    - "conf/{{ my_env }}/*"
  when: copy_conf|bool == true

- name: "Copy scripts"
  copy:
    src: "{{ item }}"
    dest: "{{ deploy_dir }}"
    owner: "{{ deploy_user }}"
    mode: 0770
  with_fileglob:
    - "scripts/*"

- name: "Copy pool-settings.xml"
  template:
    src: "pool-settings.xml.{{ my_env }}.j2"
    dest: "{{ pool_settings_dir }}/pool-settings.xml"
    mode: 0660
  
- name: "Copy unit file"
  template:
    src: "{{ service_name }}.service.j2"
    dest: "~/.config/systemd/user/{{ service_name }}.service"

- name: "Start service"
  systemd:
    scope: user
    daemon_reload: true
    name: "{{ service_name }}"
    state: started
    enabled: true

- name: "Remove passwords from pool-settings.xml"
  replace:
    path: "{{ pool_settings_dir }}/pool-settings.xml"
    regexp: '(password\">).*(</entry>)'
    replace: '\1\2'

Ошибки, проблемы

Collection ansible.posix does not support Ansible version

[WARNING]: Collection ansible.posix does not support Ansible version 2.17.0

ansible-galaxy collection install ansible.posix
Starting galaxy collection install process
[WARNING]: Collection junipernetworks.junos does not support Ansible version 2.17.0
[WARNING]: Collection frr.frr does not support Ansible version 2.17.0
[WARNING]: Collection ibm.qradar does not support Ansible version 2.17.0
[WARNING]: Collection cisco.asa does not support Ansible version 2.17.0
[WARNING]: Collection ansible.posix does not support Ansible version 2.17.0
Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.
 
ansible-galaxy collection install ansible.posix --force

Литература

How I used Ansible to automate updates at home (RedHat)
Ansible 101 with Jeff Geerling
ADV-IT - Уроки Ansible на Русском