# Ansible В данном практическом занятии рассматривается базовое использование ansible. ## Vagrant Для работы с ansible воспользуемся следующим `Vagrantfile` c тремя машинами: ```ruby Vagrant.configure("2") do |config| config.vm.define "bastion" do |c| c.vm.box = "ubuntu/lunar64" c.vm.hostname = "bastion" c.vm.network "private_network", type: "dhcp" c.vm.provision "shell", inline: <<-SHELL apt-get update -q apt-get install -yq libnss-mdns ansible SHELL end config.vm.define "node1" do |c| c.vm.box = "ubuntu/lunar64" c.vm.hostname = "node1" c.vm.network "private_network", type: "dhcp" c.vm.provision "shell", inline: <<-SHELL apt-get update -q apt-get install -yq libnss-mdns SHELL end config.vm.define "node2" do |c| c.vm.box = "ubuntu/lunar64" c.vm.hostname = "node2" c.vm.network "private_network", type: "dhcp" c.vm.provision "shell", inline: <<-SHELL apt-get update -q apt-get install -yq libnss-mdns SHELL end end ``` Все управление будем производить с машины `bastion`, для взаимодействия с другими машинами потребуется `ssh` ключ, путь до которого можно узнать в выводе команды `vagrant ssh-config`: ```console $ vagrant ssh-config bastion | grep IdentityFile IdentityFile ~/.vagrant.d/boxes/ubuntu-VAGRANTSLASH-lunar64/0/virtualbox/vagrant_insecure_key ``` Скопируем данный файл в директорию проекта с `Vagrantfile` с именем `key`. Все дальнейшие команды будем вводить находясь на машине `bastion`, в первую очередь добавив ключ пользователю `vagrant` и выставив переменную `ANSIBLE_HOST_KEY_CHECKING` в значение `False` для отключения проверки `ssh` ключей. ```console $ install -m 600 -o vagrant /vagrant/key /home/vagrant/.ssh/id_rsa $ export ANSIBLE_HOST_KEY_CHECKING=False ``` ## Inventory Для управления другими машинами нам необходимо создать [inventory][] файл с их списком, который также можно разбить по группам. Создадим файл `hosts`: ```ini [bastion] bastion.local [nodes] node1.local node2.local ``` ## Basic Usage Управлять машинами из [inventory][] можно из командной строки с помощью [команды `ansible`][cli]. С помощью опции `-i` указывается путь до [inventory][], `--key-file` указывает на `ssh` ключ с которым будет производиться подключение, `-m` задает [модуль][module] для исполнения и позиционный аргумент (в данном случае `all`) выбирает на каких машинах будет происходить выполнение. Для отключения проверки `ssh` ключей при подключении можно выставить переменную среды `ANSIBLE_HOST_KEY_CHECKING` в значение `False`. Воспользуемся [модулем][module] `ping` и [запустим проверку всех машин][adhoc]: ```console $ export ANSIBLE_HOST_KEY_CHECKING=False $ ansible -i hosts -m ping all bastion.local | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } node1.local | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } node2.local | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ``` Можно также выбрать группу для запуска, если не требуется запуск на всех машинах. Воспользуемся [модулем][module] `shell` для запуска команды на группе `nodes`: ```console $ ansible -i hosts -m shell -a 'uname -a' nodes node1.local | CHANGED | rc=0 >> Linux node1 6.2.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 14 13:42:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux node2.local | CHANGED | rc=0 >> Linux node2 6.2.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 14 13:42:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux ``` Также ограничить выполнения только определенными хостами/группами можно опцией `-l|--limit`: ```console $ ansible -i hosts -m shell -a 'hostname' nodes -l node1.local node1.local | CHANGED | rc=0 >> node1 ``` ```{note} Для получения информации по параметрам [модуля][module] можно воспользоваться командой `ansible-doc`, например `ansible-doc shell`. Получить список [модулей][module] можно командой `ansible-doc -l`. ``` ## Playbook ### Tasks Для декларативного описания задач(tasks), которые будут выполняться на машинах в [inventory][], можно описать [playbook][] в виде `yaml` файла. Плейбук может состоять из одного или нескольких `play`, которые в свою очередь содержат задачи(tasks). Также в `play` указывается группа хостов(hosts) для запуска. Сделаем плейбук, который будет устанавливать пакет `nginx` на группу `nodes` с помощью [модуля][module] `ansible.builtin.package`: ```yaml - hosts: nodes become: true tasks: - name: nginx package ansible.builtin.package: name: nginx ``` Здесь также добавлен параметр `become: true` для запроса повышения привилегий, так как для установки пакетов в систему требуются права `root`. Сохраним в файл `playbook.yaml` и запустим с помощью команды `ansible-playbook`: ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ******************************************************************************************* TASK [Gathering Facts] ********************************************************************************* ok: [node1.local] ok: [node2.local] TASK [nginx package] *********************************************************************************** changed: [node1.local] changed: [node2.local] PLAY RECAP ********************************************************************************************* node1.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 node2.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $ curl -s node1.local | grep title Welcome to nginx! $ curl -s node2.local | grep title Welcome to nginx! ``` Как видно `ansible` запускает `play` и в нем запускает ряд задач `tasks`. Первым запускается задача `Gathering Facts`, которая собирает информацию о машинах во внутренние переменные, которые потом можно использовать в других задачах. Если информация не нужна, то можно добавить параметр `gather_facts: False`: ```yaml --- - hosts: nodes become: True gather_facts: False tasks: - name: nginx package ansible.builtin.package: name: nginx ``` ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ******************************************************************************************* TASK [nginx package] *********************************************************************************** ok: [node1.local] ok: [node2.local] PLAY RECAP ********************************************************************************************* node1.local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 node2.local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Как видно задача по сбору фактов пропала, а также видно что задача `nginx package` теперь находится не в статусе `changed`, а в статусе `ok`, что означает что никаких действий не было произведено, так как пакет был установлен при прошлом запуске. ### Handlers Для того чтобы [запускать какие-либо действия только при изменениях][handlers] в `play` также можно указать список `handlers`, задачи в котором будут выполняться только при наступлении определенных событий. Добавим в основной раздел `tasks` задачу по копированию конфигурации, а в `handlers` рестарт сервиса `nginx`, который будет запускаться при изменении конфигурации: ```yaml - hosts: nodes become: True gather_facts: False tasks: - name: nginx package ansible.builtin.package: name: nginx - name: nginx config ansible.builtin.copy: src: default dest: /etc/nginx/sites-enabled/default notify: nginx restart handlers: - name: nginx restart ansible.builtin.systemd: name: nginx state: restarted ``` Сам файл конфигурации расположим рядом с плейбуком в директории `files`, которая используется по умолчанию для копирования файлов на удаленные машины. Файл конфигурации будет называться `default`: ```nginx server { listen 80; listen [::]:80; server_name localhost; location / { root /var/www/html; index index.html; } } ``` ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ***************************************************************************** TASK [nginx package] ********************************************************************* ok: [node1.local] ok: [node2.local] TASK [nginx config] ********************************************************************** changed: [node1.local] changed: [node2.local] RUNNING HANDLER [nginx restart] ********************************************************** changed: [node1.local] changed: [node2.local] PLAY RECAP ******************************************************************************* node1.local : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 node2.local : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` ### Jinja2 В `ansible` [используются шаблонизатор `jinja2`][jinja] для использования переменных как в самих плейбуках, так и в [модуле][module] `template` для шаблонизации файлов на удаленных машинах. Создадим в директории `templates` шаблон файл `index.html.j2`: ```jinja hello from {{ ansible_host }} ``` Где `ansible_host` будет указывать на машину, на которой будет выполняться задача. Дополним плейбук: ```yaml - hosts: nodes become: True gather_facts: False vars: html_dir: /var/www/html tasks: - name: nginx package ansible.builtin.package: name: nginx - name: nginx config ansible.builtin.copy: src: default dest: /etc/nginx/sites-enabled/default notify: nginx restart - name: index.html to "{{ html_dir }}" ansible.builtin.template: src: index.html.j2 dest: "{{ html_dir }}/index.html" handlers: - name: nginx restart ansible.builtin.systemd: name: nginx state: restarted ``` Здесь мы также вынесли в блок с переменными `vars` путь до html директории, чтобы показать как можно использовать переменные в самом плейбуке. ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ****************************************************************************************** TASK [nginx package] ********************************************************************************** ok: [node1.local] ok: [node2.local] TASK [nginx config] *********************************************************************************** ok: [node1.local] ok: [node2.local] TASK [index.html to "/var/www/html"] ****************************************************************** changed: [node1.local] changed: [node2.local] PLAY RECAP ******************************************************************************************** node1.local : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 node2.local : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $ curl node1.local hello from node1.local $ curl node2.local hello from node2.local ``` ### Conditionals Для задачи [можно определить условия, при которых она должна выполняться][conditional] с помощью директивы `when`: ```yaml --- - hosts: nodes become: True gather_facts: False vars: html_dir: /var/www/html tasks: - name: nginx package ansible.builtin.package: name: nginx - name: nginx config ansible.builtin.copy: src: default dest: /etc/nginx/sites-enabled/default notify: nginx restart - name: index.html to "{{ html_dir }}" ansible.builtin.template: src: index.html.j2 dest: "{{ html_dir }}/index.html" - name: node1only file ansible.builtin.copy: content: "hello from node1 only\n" dest: "{{ html_dir }}/node1only" when: ansible_host == "node1.local" handlers: - name: nginx restart ansible.builtin.systemd: name: nginx state: restarted ``` ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ****************************************************************************************** TASK [nginx package] ********************************************************************************** ok: [node2.local] ok: [node1.local] TASK [nginx config] *********************************************************************************** ok: [node2.local] ok: [node1.local] TASK [index.html to "/var/www/html"] ****************************************************************** ok: [node1.local] ok: [node2.local] TASK [node1only file] ********************************************************************************* skipping: [node2.local] changed: [node1.local] PLAY RECAP ******************************************************************************************** node1.local : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $ curl node1.local/node1only hello from node1 only $ curl node2.local/node1only 404 Not Found

404 Not Found


nginx/1.22.0 (Ubuntu)
``` Как видно задача на машине `node2.local` не выполнялась, так как для нее не выполнено условие в директиве `when`. ### Loops В `ansible` есть [несколько директив для определения цикла][loops]: `loop`, `with_*`, `until`. С помощью них можно выполнить задачу множество раз с разными переменными. Добавим задачу с циклом в плейбук: ```yaml --- - hosts: nodes become: True gather_facts: False vars: html_dir: /var/www/html tasks: - name: nginx package ansible.builtin.package: name: nginx - name: nginx config ansible.builtin.copy: src: default dest: /etc/nginx/sites-enabled/default notify: nginx restart - name: index.html to "{{ html_dir }}" ansible.builtin.template: src: index.html.j2 dest: "{{ html_dir }}/index.html" - name: node1only file ansible.builtin.copy: content: "hello from node1 only\n" dest: "{{ html_dir }}/node1only" when: ansible_host == "node1.local" - name: loop files ansible.builtin.copy: content: "{{ ansible_host }}\n" dest: "{{ html_dir}}/{{ item }}" loop: - test1 - test2 - test3 handlers: - name: nginx restart ansible.builtin.systemd: name: nginx state: restarted ``` ```console $ ansible-playbook -i hosts playbook.yaml PLAY [nodes] ****************************************************************************************** TASK [nginx package] ********************************************************************************** ok: [node2.local] ok: [node1.local] TASK [nginx config] *********************************************************************************** ok: [node1.local] ok: [node2.local] TASK [index.html to "/var/www/html"] ****************************************************************** ok: [node1.local] ok: [node2.local] TASK [node1only file] ********************************************************************************* skipping: [node2.local] ok: [node1.local] TASK [loop files] ************************************************************************************* changed: [node1.local] => (item=test1) changed: [node2.local] => (item=test1) changed: [node1.local] => (item=test2) changed: [node2.local] => (item=test2) changed: [node1.local] => (item=test3) changed: [node2.local] => (item=test3) PLAY RECAP ******************************************************************************************** node1.local : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 node2.local : ok=4 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 $ curl node1.local/test1 node1.local $ curl node2.local/test2 node2.local $ curl node1.local/test3 node1.local ``` [inventory]:https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html [cli]:https://docs.ansible.com/ansible/latest/cli/ansible.html [adhoc]:https://docs.ansible.com/ansible/latest/command_guide/intro_adhoc.html [module]:https://docs.ansible.com/ansible/latest/module_plugin_guide/modules_intro.html [playbook]:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html [handlers]:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html [jinja]:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html [conditional]:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_conditionals.html [loops]:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html