В первой части рассмотрены следующие темы:
— Конфигурационный файл Ansible, Inventory-файл
— Полезные команды Ansible
— Теги
— Переменные
— Ad-Hoc-режим
— Модули
— Debug в Ansible
В этой второй части рассмотрены следующие темы:
— Роли
— Import/Include
— Выполнение задачи на другом сервере(delegate_to)
— Однократное выполнение задачи (run_once )
— Перехват и обработка ошибок(ignore_errors|any_errors_fatal)
— Помещение вывода выполнения команды таска в переменную(register,failed_when)
— Условия выполнения таска( when)
— Ansible Vault
Работа с ролями
Роль представляет собой структурированный плейбук содержащий набор (как и минимум) тасков (task), и дополнительно — обрабработчиков событий (handler), переменных (defaults), файлов (files), шаблонов (templates), а также описание и зависимости (meta).
Создание роли
1 |
# mkdir roles && cd roles |
Создание роли с именем deploy_apache_web(при этом создается соответствующая структура каталогов)
1 |
# ansible-galaxy init deploy_apache_web |
1 |
# tree deploy_apache_web/ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
deploy_apache_web/ ├── defaults │ └── main.yml ├── files ├── handlers │ └── main.yml ├── meta │ └── main.yml ├── README.md ├── tasks │ └── main.yml ├── templates ├── tests │ ├── inventory │ └── test.yml └── vars └── main.yml |
Файл с переменными по умолчанию(defaults/main.yml)
Здесь очень удобно задавать какие-то общие переменные, а в group_vars/host_vars можно задавать нужные групповые/хостовые переменные для соответствующих групп/хостов
1 |
# cat roles/deploy_apache_web/defaults/main.yml |
1 2 |
--- destin_folder: /var/www/html |
Файлы с переменными роли по пути «/vars/filename.yml» показаны в разделе import/include
Файл с обработчиками событий(handlers/main.yml)
1 |
# cat roles/deploy_apache_web/handlers/main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 |
--- - name: reload-apache-debian-ubuntu service: name: apache2 state: reloaded when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - name: reload-apache-red-hat-centos service: name: httpd state: reloaded when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux' |
Обработчики (handlers) это задачи с уникальными именами, которые будут выполняться только в случае уведомления от другой задачи.
Обработчики(Handlers) выполняется всегда в конце плейбука, несмотря на то,что handler может вызываться несколькими тасками
Они очень удобны для перезапуска сервисов или перезагрузки системы.
Вы можете объявить их используя handler и вызвать с помощью notify.
Файл с описанием тасков(tasks/main.yml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
--- - name: Check and print linux version debug: var: ansible_os_family - block: ##### Block for Debian/Ubuntu ##### - name: Install Apache Web Server for Debian/Ubuntu apt: name: apache2 state: present - name: start and enable Apache on every boot for Debian/Ubuntu service: name: apache2 state: started enabled: yes when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - block: ##### Block for Red Hat/Centos ##### - name: install Apache web server for Red Hat/Centos yum: name: httpd state: present - name: start and enable apache on every boot for Red Hat/Centos service: name: httpd state: started enabled: yes when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux' - name: copy default content/pages to Server copy: src: "{{ item }}" dest: "{{ destin_folder }}" mode: 0555 loop: - "file1.txt" - "file2.txt" - "file3.txt" - "file4.txt" notify: - reload-apache-debian-ubuntu - reload-apache-red-hat-centos - name: Generate index.html file template: src: "index.html.j2" dest: "{{ destin_folder }}/index.html" mode: 0555 notify: - reload-apache-debian-ubuntu - reload-apache-red-hat-centos |
Файл с описанием шаблона (templates/filename.j2)
В качестве шаблонизатора Ansible использует jinja2, систему шаблонов для Python
Внутри Jinja2-шаблона можно использовать любую переменную, которая определена Ansible-ом.
Во время выполнения вышеуказанного таска все переменные в шаблон-файле index.html.j2 будут заменены их соответствующими значениями, после чего подставлены в файл index.html.j2 и скопированы файл в каталог {{ destin_folder }} на целевом сервере
1 |
# cat roles/deploy_apache_web/templates/index.html.j2 |
1 2 3 4 5 6 7 8 9 10 |
<html> <title> My Ansible page </title> <H1>The page was generated by Ansible Jinja2-template</H1> Owner of this server is {{ owner }} <br> Server Hostname: {{ ansible_hostname }} <br> Server OS Family is: {{ ansible_os_family }} <br> IP-address of this server is: {{ ansible_default_ipv4.address }} <br> </html> |
Итоговая структура роли имеет вид:
1 |
# tree roles/deploy_apache_web/ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
roles/deploy_apache_web/ ├── defaults │ └── main.yml ├── files │ ├── file1.txt │ ├── file2.txt │ ├── file3.txt │ └── file4.txt ├── handlers │ └── main.yml ├── meta │ └── main.yml ├── README.md ├── tasks │ └── main.yml ├── templates │ └── index.html.j2 ├── tests │ ├── inventory │ └── test.yml └── vars └── main.yml |
Плейбук, вызывающий роль
1 |
# cat ansible-install-configure-apache-multi-os-loop-jinja2-role.yml |
1 2 3 4 5 6 |
--- - name: install default Apache web werver and configure default page hosts: kub roles: - { role: deploy_apache_web, when: ansible_system == 'Linux' } |
Установка роли с Ansible Galaxy
Формат
1 |
# ansible-galaxy install username.role_name |
Например
1 |
# ansible-galaxy install geerlingguy.mysql |
Более подробно об Ansible Galaxy
http://docs.ansible.com/ansible/galaxy.html
Include/Import
Include и import используются для разделения сложных/больших плейбуков на составные части/файлы путем выноса некоторых тасков или переменных в отдельные файлы и включение или импортирование этих отдельных файлов с тасками(с помощью директивы include) или файлов с переменными(с помощью директивы include_vars) в главный плейбук
Это позволяет сделать плейбук более читабельным и гибким
Например, вынесим таски по созданию каталогов и файлов в отдельные файлы и подключим эти файлы в основном плейбуке
1 |
# cat ansible-create-folders-include-import.yml |
1 2 3 4 5 6 7 8 |
--- - name: create folder1 file: path: /var/www/html/folder1 state: directory mode: 0755 owner: www-data group: www-data |
1 |
# cat ansible-create-files-include-import.yml |
1 2 3 4 5 6 7 8 9 10 11 |
--- - name: create file1 copy: dest: /var/www/html/folder1/file1.txt content: | Text Line1, in file1 Text Line2, in file1 Text Line3, {{ mytext }} mode: 0644 owner: www-data group: www-data |
Также переопределим в include переменную mytext, которая задана на глобальном уровне в основном плейбуке через параметр vars
Базовый/основной плейбук имеет вид
1 |
# cat ansible-include-import.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
--- - name: example usage include and import hosts: kub vars: mytext: "Hello" tasks: - name: Ping host ping: - name: create folders include: ansible-create-folders-include-import.yml - name: create files include: ansible-create-files-include-import.yml mytext="Hello2" |
Подключение файла с переменными в роли с помощью директивы include_vars
Создадим YAML-файл с описанием переменных и подключим его в плейбук с помощью директивы include_vars
1 |
# cat roles/apache/vars/Debian.yml |
1 2 3 4 5 6 7 8 9 |
--- apache: package: name: apache2 service: name: apache2 state: started username: name: www-data |
1 |
# cat roles/apache/tasks/main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
--- # tasks file for apache - include_vars: Debian.yml when: ansible_os_family == 'Debian' - name: install apache web server package: name: "{{ apache.package.name }}" state: present tags: - apache - install - name: start apache webserver service: name: "{{ apache.service.name }}" state: "{{ apache.service.state }}" enabled: true tags: - apache - service |
Перенаправление выполнения task-а на другой сервер — delegate_to
Delegate_to — Делегирование выполнение команды на другом сервере
Например, task для удаления хоста из лоадбалансера будет выполнена на master Ansible хосте(хосте, с которого мы запускаем наш плейбук)
Это достигается за счет использования параметра delegate_to
В нашем случае
1 |
delegate_to: 127.0.0.1 |
Базовый плейбук имеет вид
1 |
# cat ansible-delegate-to.yml |
1 2 3 4 5 |
… - name: Unregister server from load balancer shell: echo This server {{ inventory_hostname }} was deregistered from our load balancer, nodename is {{ ansible_nodename }} >> /tmp/log.txt delegate_to: 127.0.0.1 … |
В результате выполнения такого плейбука на мастер ансибл сервере будет создан файл /tmp/log.txt с таким содержимым
1 |
# cat /tmp/log.txt |
1 |
This server kub.mydomain.com was deregistered from our load balancer, nodename is kub |
Также полезным примером использования delegate_to является запуск таска на мастер ансибле для того, чтобы дождаться пока сервер перезагрузится
Например, перезагружаем сервера и на мастере выполняем таск, который будет ожидать пока сервера запустятся, начнет выполнять проверку доступности сервера через 5 секунд и максимальное время ожидание составит 40 секунд
1 2 3 4 5 6 7 8 9 10 11 12 13 |
…… - name: reboot mys servers shell: sleep 3 && reboot now async: 1 poll: 0 - name: wait till my server will come up online wait_for: host: "{{ inventory_hostname }}" state: started delay: 5 timeout: 40 delegate_to: 127.0.0.1 |
Запуск таска только один раз(независимо от кол-ва серверов) — run_once
На каком из набора серверов запустится таск с указанным параметром run_once – это определается порядком серверов, указанных в inventory-файле
1 2 3 |
- name: run the database migrations command: bundle exec rake db:migrate run_once: true |
Если необходимо выполнить команды только один раз и чтобы команда выполнялась на конкретном сервере, тогда используем совместно обе опции run_once и delegate_to
1 2 3 4 |
- name: update database shell: echo UPDATING DATABASE ... run_once: true delegate_to: 127.0.0.1 |
Перехват и обработка ошибок
По умолчанию при неуспешном выполнении какого-либо таска на хосте, все остальные таски на этом хосте(и только на этом хосте) не выполняются( т.е. дефолтная опция ignore_errors: false)
Для принудительного продолжения выполнения тасков, которые находятся в плейбуке ниже/после таска, который выполнился с ошибкой, на этом хосте в параметры такого таска добавляется параметр
1 |
ignore_errors: yes |
1 2 3 4 5 6 7 8 |
- name: install unexsit packages treee apt: name: treee state: present ignore_errors: yes - name: print Hello World! shell: echo Hello World! |
Если необходимо, чтобы при неуспешном выполнении одного таска на любом хосте, немедленно прекратить дальнейшее выполнение всего плейбука на ВСЕХ хостах, то используем параметр
1 |
any_errors_fatal: true |
в глобальных параметрах плейбука
1 2 3 4 5 6 7 |
--- - name: example error handling hosts: kub any_errors_fatal: true tasks: …… |
Помещение вывода выполнения команды таска в переменную — register
Вывод результата выполнения команды помещаем в переменную myresult(параметр register) и выводим значение переменной(модуль debug)
1 2 3 4 5 6 7 |
- name: print Hello World! shell: echo Hello World register: myresult - name: Print output of previous command which stored in the variable myresult debug: var: myresult |
при запуске плейбука получаем такой вывод по заданному таску
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
TASK [Print output of previous command which stored in the variable myresult] ************************************************* ok: [kub.mydomain.com] => { "myresult": { "changed": true, "cmd": "echo Hello World", "delta": "0:00:00.002495", "end": "2019-05-02 16:32:30.615597", "failed": false, "rc": 0, "start": "2019-05-02 16:32:30.613102", "stderr": "", "stderr_lines": [], "stdout": "Hello World", "stdout_lines": [ "Hello World" ] } } |
Теперь из такого результата можно вытащить любой параметр
Полезно использовать параметры стандартного вывода команды(stdout) и стандартного вывода команды разделенного по строкам (stdout_lines), стандартного вывода ошибок(stderr) и стандартного вывода ошибок разделенного по строкам(stderr_lines) и кода выполнения/выхода команды return code(rc)
например, возьмем для работы stdout
1 2 3 4 5 6 7 |
- name: print Hello World! shell: echo Hello World register: myresult - name: Print output of previous command which stored in the variable myresult debug: var: myresult.stdout |
при запуске плейбука получаем такой вывод по заданному таску
1 2 3 4 |
TASK [Print output of previous command which stored in the variable myresult] ************************************************* ok: [kub.mydomain.com] => { "myresult.stdout": "Hello World" } |
Это позволяет делать поиск в таком выводе команды и отмечать, например, таск как выполнившийся неуспешно, если найден указанный нами паттерн/шаблон/слово
с помощью параметра
1 |
failed_when |
Например, пометим таск как выполнившийся с ошибкой, если в стандартном выводе команды в этом таске присутствует слово World с помошью параметра failed_when
failed_when: «‘World’ in myresult.stdout»
1 2 3 4 |
- name: print Hello World! shell: echo Hello World register: myresult failed_when: "'World' in myresult.stdout" |
В результате выполнения такого таска будет получена ошибка свидетельствующая о том, что указанный нами паттерн был найден в выводе команды, выполнявшейся в таске
1 2 |
TASK [print Hello World!] ***************************************************************************************************** fatal: [kub.mydomain.com]: FAILED! => {"changed": true, "cmd": "echo Hello World", "delta": "0:00:00.003088", "end": "2019-05-02 16:36:35.782446", "failed_when_result": true, "rc": 0, "start": "2019-05-02 16:36:35.779358", "stderr": "", "stderr_lines": [], "stdout": "Hello World", "stdout_lines": ["Hello World"]} |
Также удобно использовать код выхода выполнения команды
Как обычно в Linux
0- команда выполнилась успешно
Все остальные коды отличные от нуля — не успешное выполнение команды
Например, отметим таск как выполнившейся неуспешно, если код выхода команды отличный от нуля
1 2 3 4 |
- name: print Hello World! shell: echo Hello World register: myresult failed_when: myresult.rc != 0 |
В результате выполнения такого таска будет получено сообщение, свидетельствующее об успешном выполнении таска
1 2 3 4 |
TASK [Print output of previous command which stored in the variable myresult] ************************************************* ok: [kub.mydomain.com] => { "myresult.stdout": "Hello World" } |
Выполнение таска либо подключение/импорт файла в зависимости от условий — when
When — позволяет выполнять таск или подключать/импортировать другие файлы, если условие, которое указано в нем истинно
Например, выведем на экран ту или иную фразу/строку в зависимости от того, выполняется ли условие в параметре when
Например, проверим наличие файла (с помощью модуля stat) и результат проверки сохраним в переменную result( с помощью register)
В следующем таске(Print message when the file is exist) проверяется статус/состояние переменной result – если файл существует, то выведем соответствующую строку на экран(«The file is exist»)
Если файл не существует, то этот таск не будет выполняться вообще.
А выполнится следующий таск(Print message when the file is NOT exist) т.к. файла не существует и условие в when истино. Соответственно на экран будет выведена строка («The file is NOT exist «)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- name: Check whether the file is exist stat: path: /project/devops/abc.txt register: result - name: Print message when the file is exist debug: msg: "The file is exist" when: result.stat.exists - name: Print message when the file is NOT exist debug: msg: "The file is NOT exist" when: result.stat.exists == false |
Аналогично выполним/пропустим выполнение таска в зависимости от коды выхода команды
В первом таске выполним команду по проверки версии apache и запишим результат вывода команды в переменную register
Если apache2 установлен(соответственно и его версию мы получим корректно), то выполнится второй таск, который выведем на экран вывод команды указанной в первой таске. Третий таск в этом случае ожидаемо будет пропущен
Если apache2 не установлен, тогда команда в первом таске выполнится с ошибкой и соответственно код выхода такой команды будет отличным от нуля.
В результате чего второй таск выполняться не будет(т.к. условие в when не истинно/ не выполняется), а выполнится третий таск, который установит пакет apache2
Также важно отметить, что используется опция ignore_errors, которая позволяет продолжить выполнение ниже следующих тасков для хоста при неуспешном выполнении таска, для которого установлена эта опция,(по умолчанию, ансибл немедленно прекращает выполнение всех ниже следующих тасков для хоста, при неуспешном выполнении текущего таска)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- name: verify apache2 version command: /usr/sbin/apache2 -v register: version ignore_errors: True - name: print apache2 version debug: msg: "{{ version }}" when: "version.rc == 0" - name: install apache2 apt: name: apache2 state: present when: "version.rc != 0 " |
Аналогично при использовании переменных
Например, переменных(ansible_), которые становятся доступными после собрания фактов
1 2 3 4 5 |
- name: reload-apache-debian-ubuntu service: name: apache2 state: reloaded when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' |
Еще один пример
Первый таск будет выполняться только когда переменная foo определена.
Второй таск будет выполняться только тогда, когда переменная bar НЕ определена
Если второй таск будет выполняться, то ожидаемо выполнится с ошибкой(модуль fail)
1 2 3 4 5 6 7 8 |
# vars: # foo: Hello # bar: World tasks: - shell: echo "We use {{ foo }} variable!" when: foo is defined - fail: msg="This play requires 'bar' variable" when: bar is undefined |
Ansible Vault
Создание шифрованного файла с секретной информацией
1 |
# ansible-vault create mysecret.txt |
Вводим пароль для шифрования/дешифрования файла
Далее открывается текстовый редактор по-умолчанию для наполнения файла
Просмотр текущего файла(необходимо будет ввести пароль, установленный при создании шифрованого файла предыдущей командой)
1 |
# ansible-vault view mysecret.txt |
Eсли просматривать файл в открытом виде файл будет содержать зашифрованный текст
1 |
# cat mysecret.txt |
1 2 |
$ANSIBLE_VAULT;1.1;AES256 61386330663764383636…… |
Изменение содержания/редактирование файла
1 |
# ansible-vault edit mysecret.txt |
Изменение пароля, установленного при создании шифрованного файла
1 |
# ansible-vault rekey mysecret.txt |
1 2 3 4 |
Vault password: New Vault password: Confirm New Vault password: Rekey successful |
Создадим и зашифруем плейбук, в котором хранится некоторый пароль(mypassword)
1 |
# cat ansible-vault.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--- - name: example ansible usage hosts: kub vars: admin_password: mypassword tasks: - name: create config file copy: dest: "/var/www/html/file5.txt" content: | port = 9092 log = 7days user = admin password = {{ admin_password }} |
Шифруем плейбук
1 |
# ansible-vault encrypt ansible-vault.yaml |
1 2 3 |
New Vault password: Confirm New Vault password: Encryption successful |
В открытом виде информация не читаемая
1 |
# cat ansible-vault.yaml |
1 2 |
$ANSIBLE_VAULT;1.1;AES256 3334316430393737373563396135… |
Для просмотра плейбука используем команду
1 |
# ansible-vault view ansible-vault.yaml |
Расшифровать плейбук
1 |
# ansible-vault decrypt ansible-vault.yaml |
1 2 |
Vault password: Decryption successful |
Содержимое файла станет доступным в открытом виде
Зашифруем снова
1 |
# ansible-vault encrypt ansible-vault.yaml |
Для запуска зашифрованного плейбука необходимо использовать параметр/опцию
1 |
--ask-vault-pass |
В противном случае попытка дешфрования плейбука будет неуспешной
1 |
# ansible-playbook ansible-vault.yaml |
1 |
ERROR! Attempting to decrypt but no vault secrets found |
Для корректной расшифровке плейбука используем параметр —ask-vault-pass
1 |
# ansible-playbook ansible-vault.yaml --ask-vault-pass |
Для запуска шифрованного плейбука без пароля используем опцию
1 |
--vault-password-file |
в которой указываем файл, содержащий пароль
1 |
# cat myvaultpass.txt |
1 |
myvaultpassword |
1 |
# ansible-playbook ansible-vault.yaml --vault-password-file myvaultpass.txt |
Наряду с шифрованием всего плейбука Ansible поддерживает шифрование отдельных
строк/слов/текста
Например, зашифруем указанный в плейбуке пароль mypassword
Для этого используем команду
1 |
ansible-vault encrypt_string |
Командой спросит пароль, которым будут шифроваться данные
После чего предложит вести текст, который нужно зашифровать
1 |
# ansible-vault encrypt_string |
1 2 3 4 5 6 7 8 9 10 11 |
New Vault password: Confirm New Vault password: Reading plaintext input from stdin. (ctrl-d to end input) mypassword!vault | $ANSIBLE_VAULT;1.1;AES256 32396362373634653366336264343935303266356361623638346431653266633362616230666263 3763343732326231646438333165656630306632613437360a386530396666626235616634376534 33656237623837303232663462616335383531376664396438633335663163366332353730333963 6164393262613263370a643661643739623165386434383738636664616266383530363866373865 6532 Encryption successful |
Также можно использовать следующую команду для достиженя такого же результата(получение шифрованного пароля mypassword)
1 |
# echo -n "mypassword" | ansible-vault encrypt_string |
Теперь в плейбуке вместо пароля в чистом виде, используем его зашифрованную ansible-vault-ом версию
1 2 3 4 5 6 7 |
!vault | $ANSIBLE_VAULT;1.1;AES256 32396362373634653366336264343935303266356361623638346431653266633362616230666263 3763343732326231646438333165656630306632613437360a386530396666626235616634376534 33656237623837303232663462616335383531376664396438633335663163366332353730333963 6164393262613263370a643661643739623165386434383738636664616266383530363866373865 6532 |
Т.е. пароль в плейбуке пароль будет иметь вид
1 2 3 4 5 6 7 8 |
vars: admin_password: !vault | $ANSIBLE_VAULT;1.1;AES256 32396362373634653366336264343935303266356361623638346431653266633362616230666263 3763343732326231646438333165656630306632613437360a386530396666626235616634376534 33656237623837303232663462616335383531376664396438633335663163366332353730333963 6164393262613263370a643661643739623165386434383738636664616266383530363866373865 6532 |
Также, как и с шифрованием всего плейбука, при шифровании отдельных его строк, значений(например, шифрование пароля, которое мы выполнили) необходимо использовать либо опцию —ask-vault-pass либо опцию —vault-password-file
Без указаний одной из этих опций ожидаемо дешифровка зашифрованного пароля будет неуспешной
1 |
# ansible-playbook ansible-vault.yaml |
1 2 3 4 5 |
… TASK [create config file] ***************************************************************************************************** fatal: [kub.mydomain.com]: FAILED! => {"msg": "Attempting to decrypt but no vault secrets found"} to retry, use: --limit @/etc/ansible/learning/ansible-vault.retry … |
1 |
# ansible-playbook ansible-vault.yaml --ask-vault-pass |
1 2 3 4 |
Vault password: … TASK [create config file] ***************************************************************************************************** changed: [kub.mydomain.com] |
На хосте, на котором выполнялся плейбук, согласно плейбуку сгенерировался файл с корректным паролем
1 |
# cat /var/www/html/file5.txt |
1 2 3 4 |
port = 9092 log = 7days user = admin password = mypassword |