в ansible, debian, ubuntu

Знакомство с Ansible. Часть 5: роли, условия и циклы

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

В завершающей части знакомства с Ansible давайте разберемся с условиями и циклами, а также научимся организовывать playbook в роли!

Для сокращения количества задач в наборах инструкций (playbook) разумно использовать циклы. Например, иногда требуется установить несколько пакетов на один и тот же удаленный хост или выполнить несколько операций над одним и тем же ресурсом.

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

---
- hosts: test
  tasks:

  - name: Install packages
    apt: name={{ item }} state=latest
    with_items:
    - htop
    - mytop
    - wget
    sudo: yes

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

ansible-playbook install_packages.yml 

PLAY [test] ******************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [test-1]

TASK: [Install packages] ****************************************************** 
changed: [test-1] => (item=htop,mytop,wget)

PLAY RECAP ******************************************************************** 
test-1                 : ok=2    changed=1    unreachable=0    failed=0   

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

---
- hosts: test
  tasks:
   - name: Add test users
     user: name={{ item.name }} state=present groups={{ item.groups }}
     with_items:
      - { name: 'user1', groups: 'adm'  }
      - { name: 'user2', groups: 'lpadmin' }
     sudo: yes

Результат выполнения данного playbook будет выглядеть так:

ansible-playbook add_users.yml 

PLAY [test] ******************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [test-1]

TASK: [Add test users] ******************************************************** 
changed: [test-1] => (item={'name': 'user1', 'groups': 'adm'})
changed: [test-1] => (item={'name': 'user2', 'groups': 'lpadmin'})

PLAY RECAP ******************************************************************** 
test-1                 : ok=2    changed=1    unreachable=0    failed=0   

Примечание. Больше примеров с использованием циклов можно найти в официальной документации по Ansible.

Ansible выполняет задачи в порядке их следования в наборе инструкций, но бывают случаи, когда требуется выполнить только часть задач. В третьей части нашего цикла мы уже пробовали правильно устанавливать web-сервер Apache на разные дистрибутивы с помощью переменных. Чтобы задачи выполнялись только в определенном случае, можно (и нужно) указывать условия с помощью when:.

В условиях в Ansible для сравнения используются == (равно), != (не равно), > (больше), < (меньше), >= (больше равно), <= (меньше равно). Можно также указать несколько условий с помощью операторов and (и) и or (или). Для проверки вхождения символа или подстроки в строку используются операторы in и not.

Для проверки работы некоторых условий в Ansible создадим playbook следующего содержания:

---
- hosts: test
  tasks:

  - name: Check OS family
    debug: msg="This is my OS"
    when: ansible_os_family == "Debian"

  - name: Check if Apache2 is installed
    command: dpkg-query -W apache2
    register: apache2_check

  - name: Print message if apache installed
    debug: msg="Apache2 is installed on remote host"
    when: "'apache2' in apache2_check.stdout"

  - name: Check if admin logged
    command: who
    register: who_check

  - name: Print message if user admin not logged
    debug: msg="User admin is not logged on remote host"
    when: not 'admin' in who_check.stdout

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

ansible-playbook debug.yml 

PLAY [test] ******************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [test-1]

TASK: [Check OS family] ******************************************************* 
ok: [test-1] => {
    "msg": "This is my OS"
}

TASK: [Check if Apache2 is installed] ***************************************** 
changed: [test-1]

TASK: [Print message if apache installed] ************************************* 
ok: [test-1] => {
    "msg": "Apache2 is installed on remote host"
}

TASK: [Check if admin logged] ************************************************* 
changed: [test-1]

TASK: [Print message if user admin not logged] ******************************** 
ok: [test-1] => {
    "msg": "User admin is not logged on remote host"
}

PLAY RECAP ******************************************************************** 
test-1                 : ok=6    changed=2    unreachable=0    failed=0   

Напоследок у нас остается немного «магии». В архитектуре проекта обычно сервера разделяются по своему предназначению и выполняют определенную роль — web-сервер, сервер баз данных, почтовый сервер и т. д. Каждому из серверов требуется отличающийся набор пакетов и настроек для правильного выполнения своей роли. С ростом количества серверов будет расти количество наборов инструкций (playbook), которые будут использоваться повторно.

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

---
- hosts: servers
  roles:
     - web
     – db
     - post

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

site.yml
servers.yml
roles/
   web/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   db/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   post/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/

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

Путь к каталогу с ролями можно задать с помощью параметра roles_path в конфигурационном файле Ansible. При указании ролей можно использовать тэги.

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

  • если существует roles/.../tasks/main.yml, то задачи из этого файла будут добавлены в набор инструкций;
  • если существует roles/.../handlers/main.yml, то обработчики из этого файла будут добавлены в набор инструкций;
  • если существует roles/.../vars/main.yml, то переменные из этого файла будут добавлены в набор инструкций;
  • если существует roles/.../meta/main.yml, то любые роли-зависимости будут добавлены в список ролей;
  • задача копирования может ссылаться на файл в roles/.../files без указания абсолютного или относительного пути;
  • скриптовая задача может ссылаться на скрипт в roles/.../files без указания абсолютного или относительного пути;
  • задача шаблонизации может ссылаться на roles/.../templates без указания абсолютного или относительного пути;
  • импортируемые задачи могут ссылаться на файлы в roles/.../tasks без указания абсолютного или относительного пути.

О ролях можно писать очень много и долго, поэтому я лучше порекомендую Ansible Galaxy — крупнейший репозиторий ролей Ansible, там можно брать уже готовые роли (например, для обучения) или делиться своими ролями.

На этом все, знакомство с Ansible завершено, но это только вершина айсберга — намного больше предстоит узнать активно используя Ansible при настройке IT-инфраструктуры.

Добавить комментарий

28 Комментария

  1. Отлично! Потратив полчаса на прочтение этих пяти страниц, я узнал больше, чем в пятидневном изучении «Полного руководства» — это был ужас! Как будто депутат Федоров писал, куча бессмысленных фраз и какая-то белиберда с упоминанием сущностей, но без описания их. А здесь кратко, но доходчиво. Автор молодец. Спасибо!

  2. Евгений, здравствуйте! =)

    Скажите пожалуйста, как запускать отдельную роль? И как запускать на одной группе хостов или на одном хосте из всего файла inventory?

    • вместо одного хоста можно вписать целую группу хостов из файла inventory, если там они идут в формате:

      [managers]
      manager1
      manager2
      manager3
      ...
      [web]
      web1
      web2
      web3
      [db]
      db1
      db2
      

      тогда можно в hosts вписать просто db, и роль будет запущена на хостах db1 и db2

  3. Евгений, добрый день! Как можно правильно назвать кучу папок (для ansible) с ролями, переменными, файлами — «плейбук»? Мне кажется что плейбуком можно назвать один файл. Проект?

    • Я с этим не заморачивался, у меня все лежит в папке ansible:
      # find . -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
      .
      |____ansible.cfg
      |____files
      | |____cupsd.conf
      | |____fstab
      | |____nsswitch.conf
      | |____ntp.conf
      | |____skel
      | | |____.bash_logout
      | | |____.bashrc
      | |____skel.tar.gz
      | |____sources.list
      | |____speedtest-cli
      | |____speedtest.sh
      | |____zabbix_agentd.conf
      |____hosts
      |____logs
      | |____ansible.log
      |____playbooks
      | |____copy_files.yml
      | |____copy_skel.yml
      | |____debug.yml
      | |____deploy_io_test.yml
      | |____install_apache.yml
      | |____install_bash_profile.yml
      | |____install_chrome.yml
      | |____install_gxneur.yml
      | |____install_htop.yml
      | |____install_Intel_Driver.yml
      | |____install_iperf.yml
      | |____install_monit.yml
      | |____install_ntpdate.yml
      | |____install_packages.yml
      | |____install_rsync.yml
      | |____mkdir.yml
      | |____purge_packages.yml
      | |____reboot.yml
      | |____reconfigure_cups.yml
      | |____remove_apt_sources.yml
      | |____roles
      | | |____bashrc
      | | | |____files
      | | | | |____.bashrc
      | | | | |____bash.bashrc
      | | | |____tasks
      | | | | |____main.yml
      | | |____chrome
      | | | |____tasks
      | | | | |____main.yml
      | | |____htop
      | | | |____defaults
      | | | | |____main.yml
      | | | |____files
      | | | | |____etc
      | | | | | |____skel
      | | | | | | |____.config
      | | | | | | | |____htop
      | | | | | | | | |____htoprc
      | | | |____tasks
      | | | | |____main.yml
      | | |____install_packages
      | | | |____tasks
      | | | | |____main.yml
      | | |____Intel_HD
      | | | |____files
      | | | | |____grub
      | | | | |____intel1_amd64.deb
      | | | |____tasks
      | | | | |____main.yml
      | | |____monit
      | | | |____defaults
      | | | | |____main.yml
      | | | |____handlers
      | | | | |____main.yml
      | | | |____tasks
      | | | | |____configure.yml
      | | | | |____install.yml
      | | | | |____logging.yml
      | | | | |____main.yml
      | | | | |____services.yml
      | | | |____templates
      | | | | |____etc
      | | | | | |____monit
      | | | | | | |____conf.d
      | | | | | | | |____metrics
      | | | | | | |____monitrc
      | | |____munin-node
      | | | |____defaults
      | | | | |____main.yml
      | | | |____handlers
      | | | | |____main.yml
      | | | |____tasks
      | | | | |____main.yml
      | | | |____templates
      | | | | |____munin-node.conf.j2
      | | | | |____plugin-conf.j2
      | | | |____tests
      | | | | |____inventory
      | | | | |____test1.yml
      | | | | |____test2.yml
      | | | | |____test_vars.yml
      | | | |____vars
      | | | | |____Debian.yml
      | | | | |____RedHat.yml
      | | |____mytop
      | | | |____tasks
      | | | | |____main.yml
      | | |____newrelic-sysmond
      | | | |____tasks
      | | | | |____main.yml
      | | | |____templates
      | | | | |____nrsysmond.cfg.j2
      | | | |____vars
      | | | | |____main.yml
      | | |____ntp
      | | | |____handlers
      | | | | |____main.yml
      | | | |____tasks
      | | | | |____main.yml
      | | | |____templates
      | | | | |____ntp.conf.j2
      | | | |____vars
      | | | | |____main.yml
      | | |____rsync
      | | | |____tasks
      | | | | |____CentOS.yml
      | | | | |____main.yml
      | | | | |____Ubuntu.yml
      | | | |____vars
      | | | | |____CentOS.yml
      | | | | |____main.yml
      | | | | |____Ubuntu.yml
      | | |____testrole
      | | | |____tasks
      | | | | |____main.yml
      | | | |____templates
      | | | | |____file
      | | | |____vars
      | | | | |____main.yml
      | |____safe_upgrade.retry
      | |____safe_upgrade.yml
      | |____test.yml
      | |____useradd.yml

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

    • Запустить выполнение плейбука с конкретной задачи (и выполнять остальные до конца плейбука) можно так:

      ansible-playbook debug.yml --start-at-task="Debug"

      В вашем случае также подойдет запуск playbook-а з параметром --step

      ansible-playbook install_nginx.yml --step --start-at-task "Debug"

      В случае использования --step можно самостоятельно решать, какую задачу выполнять ( y ), а какую пропускать ( n )

      • Спасибо! А если мне нужно автоматически запустить только один плейбук, один раз, например из Jenkins? То есть я могу запустить один плейбук, но хочу чтобы он сработал к разным хостам по-разному.

  5. Решение такое — добавить в файл инвентария строку

    127.0.0.1 ansible_connection=local

    Мне это нужно для того, что я собираюсь собирать пакеты на локальной машине, через Jenkins, а потом выкладывать на удаленные сервера c помощью Ansible. Конечно, и в самом Jenkins есть такая возможность после сборки произвести деплой, но это только для одного сервера. Мне же нужно произвести деплой на несколько серверов, и присвоить права.

      • Вот и не понимаю как мне это сделать. Даже сначала не могу:


        — hosts: test
        tasks:
        — find: paths=»/tmp/» patterns=»*.jar» recurse=yes
        register: file_to_copy
        — synchronize: src={{ item }} dest=/home/user/warjar/ delete=yes recursive=yes
        with_items: file_to_copy.stdout_lines

        Делаю вот так, но не работает. В /tmp/ точно есть файл *.jar для теста

        • а если тестировать с одним файлом, без использования цикла? Мне кажется тут дело как раз в цикле, возможно file_to_copy.stdout_lines неправильно передает данные в секцию with_items (может отступы/пробелы/дефис теряются)

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

          ...
          with_items:
            - item1
            - item2
            - item3
          • Сработало вот так:

            — hosts: server
            tasks:
            — find: paths=»/var/lib/jenkins/workspace/» patterns=»*.jar» recurse=yes
            register: file_to_copy
            delegate_to: localhost
            — copy: src={{ item.path }} dest=/tmp/warjar/
            with_items: file_to_copy.files

            Вот этой строкой: delegate_to: localhost мы говорим что этот таск нужно выполнить на локалхосте, который тоже есть в файле инвентария, а все остальное — на сервере. Этим мы нашли и скопировали файлы с локалхоста на сервер.

  6. Спасибо! Прекрасный цикл статей, пожалуй лучший из того, что я видел! Особенно понравилось то, что Вы привели базовые знания синтаксиса YAML, мне кажется этого никто не делал. До этого я долго мучался с тем, что у меня ничего не работает, просто скопировав из чьего-то блога пример playbook’a.

    Сейчас у меня такая задача, не знаю как ее решить. Мне нужно из двух папок на локалхосте скопировать в две папки на сервере (а если их нет, то создать их (папки) с правами пользователя) скопировать файлы, и если в папке есть файлы, то удалить их по маске (.jpg в первой папке, .png — во второй), так как они старые и не нужны, а скопировать туда новые. То есть сначала удалить там файлы, в нужных папках, если папок нет, то создать их с правами, и скопировать туда файлы. Совсем запутался и не знаю как это сделать, не могли бы Вы посоветовать, как это сделать?

    • Давайте вместе подумаем над этой задачей ))

      Для начала вспомним, что ansible поддерживает свойство идемпотентности (https://ru.wikipedia.org/wiki/%D0%98%D0%B4%D0%B5%D0%BC%D0%BF%D0%BE%D1%82%D0%B5%D0%BD%D1%82%D0%BD%D0%BE%D1%81%D1%82%D1%8C). Следовательно, в плейбуке обязательно надо прописать создание папок на удаленном хосте (если папок нет — они будут созданы, если они есть — шаг будет пропущен).

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

      Вот первый:

      # cat mkdir.yml
      ---
      - hosts: test
        vars:
          dir: /home/admin/testdir
        become: yes
        tasks:
      
        - name: Create directory
          file: path={{ dir }} state=directory owner=admin group=admin mode=0777

      а вот второй:

      # cat copy_files.yml
      ---
      - hosts: d-donchenko
        become: yes
        tasks:
      
        - name: Copy sources.list
          copy: src=~/ansible/files/sources.list dest=/etc/apt/sources.list group=root owner=root mode=0664

      Еще для решения этой задачи можно использовать модуль synchronize (работает по принципу rsync), думаю это будет выглядеть примерно так:

      ---
      - hosts: test
        become: yes
        tasks:
      
        - name: Create directory
          file: path={{ item }} state=directory owner=admin group=admin mode=0777
          with_items:
          - /home/admin/testdir1
          - /home/admin/testdir2
      
        - name: Sync files to remote host
          # Synchronize and delete files in dest on the remote host that are not found in src of localhost.
          synchronize: src={{ item.src }} dest={{ item.dest }} delete=yes recursive=yes
          with_items:
            - { src: '/local/testdir1', dest: '/home/admin/testdir1' }
            - { src: '/local/testdir2', dest: '/home/admin/testdir2' }
      

      Проверьте и отпишитесь о результатах ))

      • Спасибо за ответ!

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

        В документации есть примеры вроде:

        http://docs.ansible.com/ansible/copy_module.html

        copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode=»u+rw,g-wx,o-rwx»

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

        • параметр src (source, источник) указывает откуда брать файлы (где они лежат на локалхосте)

          параметр dest (destination, назначение) указывает куда ложить файлы (куда складывать на удаленный хост)

          Но это решение задачи «в лоб», когда больше ничего не помогает. Попробуйте сделать с модулем synchronize, как я написал выше — это должно быть красивее ))

          • Спасибо, Евгений!

            Еще такой вопрос: как мне сделать из Ansible поиск на локалхосте?

            Хоть и стоит openssh-server, но все равно ругается:

            Failed to connect to the host via ssh: Permission denied (publickey,password).\r\n»

          • Крайне странная задача )))
            Используйте обычный поиск на локалхосте с помощью find

            Смысл использования Ansible как раз и состоит в управлении удаленными хостами, а не локалхостом, поэтому в данном вопросе я вам ничем не могу помочь )