# Ansible Основы Ansible для новичка Эта статья не замена [https://docs.ansible.com/](https://docs.ansible.com/), а скорее 101 - база, которая поможет начать понимать основные принципы работы # Что такое Ansible Ansible - в каноничном определении, система управления конфигурациями. Чуть более понятно и обобщенно - это инструмент с помощью которого можно автоматизировать практически любую задачу, которую можно выполнить “руками в консоли” - от подготовки сервера до деплоя конечного приложения. Важно - Ansible не является “средством запуска shell-скриптов”, скрипты в целом для Ansible - считаются плохой практикой (крайне полезная [статья](https://habr.com/ru/post/536340/) на тему). # Как работает Ansible В отличии от Puppet, Ansible использует SSH для выполнения play на целевых хостах и не требует какого-либо агента. Из этого следует важная особенности - там где puppet-agent сам ходил в мастер, предоставлял факты о своем хосте и запрашивал изменения, в случае с Ansible - изменения не “проиграют”, пока явно их не “сыграть”. Исходя из того что используется SSH, логично что машина, с которой будет пушиться конфигурация, должен иметь SSH доступ на управляемый хост и при этом даже не обязательно быть sudoers (правда это сильно ограничивает в том, что может быть исполнено). # Из чего строится проект Ansible и как с ним работать Основным элементом Ansible является **play** - то что нужно “сыграть”, в какое состояние нужно привести какой-то аспект системы. Убедиться что на хосте существует директория, файл с содержимым, запущенный docker контейнер, etc. - это все play. Play чаще всего не существует сам по себе - из него строится **playbook** и прямой перевод говорит сам за себя - “сборник, того что нужно сыграть” и если play это “логический” элемент, то playbook это его переложение “на файл”. При этом сам play состоит из **tasks** и **roles**, и если с tasks относительно просто - это вызов указанного модуля, “*функции”* с параметрами и переменными, то roles можно воспринимать скорее как пакет - отдельная группа файлов с task’ами, handler’ами, переменными с четкой иерархией и структурой. У Ansible для этого есть даже свой “пакетный менеджер” - `ansible-galaxy`, те кто хоть раз сталкивался с `pip` сразу заметят сходство. Важно - использование одновременно tasks и roles считается плохой практикой. (крайне полезная [статья](https://habr.com/ru/post/508762/) на тему 2) Но знать “что сыграть” не достаточно, нужно также знать “где сыграть”, в терминал Ansible это **inventory** - список хостов и их групп, на которых будет “сыгран” play. Рассмотрим на конкретных примерах и навчнем с файла inventory. Файлы inventory поддерживаются в форматах ini и yaml | toml ```bash [project:children] cli web [cli:children] cli_preprod cli_prod [cli_preprod] cli1.domain.tech [cli_prod] cli2.domain.tech [web:children] web_preprod web_prod [web_preprod] web1.domain.tech [web_prod] web2.domain.tech ``` ```yaml all: children: project: children: cli: children: cli_preprod: hosts: cli1.domain.tech cli_prod: hosts: cli2.domain.tech web: children: web_preprod: hosts: web1.domain.tech web_prod: hosts: web2.domain.tech ``` Не рекомендуется использовать “-” в именах групп - это не ошибка, но надоедливый Warning: > [WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details > Здесь мы задаем общую родительскую группу project (на самом деле, она нужна лишь для удобства чтения, и дальше будет видно почему), которая имеет 2х наследников - cli и web, каждый из которых, в свою очередь, так же группа со своими наследниками - *_preprod и *_prod. Чтобы проверить, что inventory собран правильно, выполним первую команду: ```bash $ ansible-inventory --graph -i project.hosts @all: |--@project: | |--@cli: | | |--@cli_preprod: | | | |--cli1.domain.tech | | |--@cli_prod: | | | |--cli2.domain.tech | |--@web: | | |--@web_preprod: | | | |--web1.domain.tech | | |--@web_prod: | | | |--web2.domain.tech |--@ungrouped: ``` По построенному графу видно, что у нас нет хостов вне групп, а значит в данном случае project == all. Играя playbook с таким inventory, можно точно указать, где именно нужно выполнить - только на cli, или только на web, или только preprod, или просто конкретный хост. Попробуем выполнить первую задачу - пингануть хосты. CLI Ansible предусматривает 2 утилиты для проигрывания: `ansible` и `ansible-playbook` - первая для того чтобы “быстро и просто” выполнить какой-либо модуль, вторая - для того чтобы сыграть целиком playbook. ```bash $ ansible -m ping -i project.hosts.yaml all cli1.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } cli2.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } web1.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } web2.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ``` ```bash $ ansible -m ping -i project.hosts.yaml cli_preprod cli1.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ``` ```bash $ ansible -m ping -i project.hosts.yaml cli2.domain.tech cli2.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ``` ```bash $ ansible -m ping -i cli2.domain.tech, all cli2.domain.tech | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ``` Выше показан запуск модуля ping на все хосты из inventory, конкретную группу и отдельный хост, а также хак - как запустить модуль для хоста без использования файла inventory. Писать каждый раз `-m ping` уже не удобно, а для выполнения задачи модулей почти всегда будет сильно больше одного. Команду конечно можно добавить в Makefile, но не будет извращаться и напишем playbook: ```yaml # ping.yaml --- - name: "our play" # имя для play hosts: "all" # группа хостов/хост по умолчанию tasks: - name: "ping hosts" # имя task ping: # использование встроенного модуля ping ``` И запустим на нашем inventory: ```bash $ ansible-playbook ping.yaml -i project.hosts PLAY [our playbook] ******************************************************************************* TASK [Gathering Facts] **************************************************************************** ok: [cli1.domain.tech] ok: [cli2.domain.tech] ok: [web1.domain.tech] ok: [web2.domain.tech] TASK [ping hosts] ********************************************************************************* ok: [cli1.domain.tech] ok: [cli2.domain.tech] ok: [web1.domain.tech] ok: [web2.domain.tech] PLAY RECAP **************************************************************************************** cli1.domain.tech : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 cli2.domain.tech : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 web1.domain.tech : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 web2.domain.tech : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Лимитирование inventory для `ansible-playbook` работает также как и для `ansible` (за исключением необходимости ключа `-l`) Иногда бывает так, что базовых прав пользователя недостаточно для выполнения нужного действия, нужно sudo. Для примера изменим playbook, чтобы он создавал директорию /opt/test на конечном хосте: ```yaml --- - name: "our playbook" hosts: "all" tasks: - name: "ping hosts" ping: - name: "ensure direcories" file: state: directory path: "/opt/test" ``` И запустим для группы cli_preprod: ```bash $ ansible-playbook ping.yaml -i project.hosts -l cli_preprod -DC PLAY [our playbook] ******************************************************************************* TASK [Gathering Facts] **************************************************************************** ok: [cli1.domain.tech] TASK [ping hosts] ********************************************************************************* ok: [cli1.domain.tech] TASK [ensure direcories] ************************************************************************** --- before +++ after @@ -1,4 +1,4 @@ { "path": "/opt/test", - "state": "absent" + "state": "directory" } changed: [cli1.domain.tech] PLAY RECAP **************************************************************************************** cli1.domain.tech : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Обратите внимание на recap - `ok | changed | unreachable | failed | skipped | rescued | ignored` - сводка проигрывания playbook и вытекающее из этого предупреждение: не стоит сразу проигрывать playbook, если нет уверенности что используемые tasks не приведут систему в нежелательное состояние (будет остановлен/запущен не тот демон, удален/добавлен не тот пользователь, etc.). Поэтому рекомендую сперва проигрывать в check_mode с включением diff_mode - `--diff|-D --check|-C` . (Однако не каждый task может быть выполнен с check|diff модом, за подробностями - [в документацию)](https://docs.ansible.com/ansible/latest/collections/index.html) Если зайти на хост и проверить содержимое /opt, директории test там не окажется - check_mode наглядно. ```bash [root@cli1.domain.tech opt]$ ll total 16 drwxr-xr-x 4 root root 4096 Feb 17 01:59 ./ drwxr-xr-x 19 root root 4096 Feb 8 19:15 ../ drwxr-xr-x 2 root root 4096 Dec 3 17:23 bin/ drwx--x--x 4 root root 4096 Oct 28 18:22 containerd/ lrwxrwxrwx 1 root root 11 Oct 26 10:55 puppetlabs -> /etc/puppet/ ``` Допустим мы уверены что хотим создать директорию, и никаких сайд-эффектов recap не показал - запускаем без `-DC`: ```bash $ ansible-playbook ping.yaml -i project.hosts -l cli_preprod PLAY [our playbook] ********************************************************************************************************************************************* TASK [Gathering Facts] ****************************************************************************************************************************************** ok: [cli1.domain.tech] TASK [ping hosts] *********************************************************************************************************************************************** ok: [cli1.domain.tech] TASK [ensure direcories] **************************************************************************************************************************************** fatal: [cli1.domain.tech]: FAILED! => {"changed": false, "msg": "There was an issue creating /opt/test as requested: [Errno 13] Permission denied: b'/opt/test'", "path": "/opt/test"} PLAY RECAP ****************************************************************************************************************************************************** cli1.domain.tech : ok=2 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0 ``` 1. ansible-playbook 2. ??? 3. ??? 4. ~~PROFIT!!!~~ failed: Что не так? Попробуем сделать это руками: ```bash [root@cli1.domain.tech opt]$ mkdir test mkdir: cannot create directory ‘test’: Permission denied [root@cli1.domain.tech opt]$ sudo !! && ll sudo mkdir test && ll total 20 drwxr-xr-x 5 root root 4096 Feb 17 02:10 ./ drwxr-xr-x 19 root root 4096 Feb 8 19:15 ../ drwxr-xr-x 2 root root 4096 Dec 3 17:23 bin/ drwx--x--x 4 root root 4096 Oct 28 18:22 containerd/ lrwxrwxrwx 1 root root 11 Oct 26 10:55 puppetlabs -> /etc/puppet/ drwxr-xr-x 2 root root 4096 Feb 17 02:10 test/ ``` Вывод - нам нужна [эскалация привилегий](https://docs.ansible.com/ansible/latest/user_guide/become.html). Есть несоклько вариантов сделать это - можно указать `become: true` в декларации конкретного task/play/ или сыграть playbook с флагом `—become|-b`: ```bash $ ansible-playbook ping.yaml -i project.hosts -l cli_preprod -b PLAY [our playbook] ********************************************************************************************************************************************* TASK [Gathering Facts] ****************************************************************************************************************************************** ok: [cli1.domain.tech] TASK [ping hosts] *********************************************************************************************************************************************** ok: [cli1.domain.tech] TASK [ensure direcories] **************************************************************************************************************************************** ok: [cli1.domain.tech] PLAY RECAP ****************************************************************************************************************************************************** cli1.domain.tech : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Хорошо, но допустим у нас большой playbook, но нужно выполнить оттуда только несколько play - легким способом сделать такое выполнение удобным являются **tags** - передавая список `—tags|-t` аргументом `ansible-playbook`. Но для начала изменим playbook - разобъем tasks по отдельным play и добавим им tags: ```yaml --- - name: "ping hosts" hosts: "all" tags: [check] tasks: - ping: - name: "ensure direcories" hosts: "all" tags: [dirs] tasks: - file: state: directory path: "/opt/test" ``` Тогда для того чтобы только пингануть хосты, достаточно указать `-t=check` - в таком случае все play не имеющие check в своих tags будут проигнорированы. ```bash $ ansible-playbook ping.yaml -i project.hosts -l cli_preprod -b -t=check PLAY [ping hosts] ********************************************************************************* TASK [Gathering Facts] **************************************************************************** ok: [cli1.domain.tech] TASK [ping] *********************************************************************** ok: [cli1.domain.tech] PLAY [ensure direcories] ************************************************************************** PLAY RECAP **************************************************************************************** cli1.domain.tech : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Итак, мы проверяем что хосты пингуются и что на них по указанному пути существует директория. Теперь мы хотим, чтобы внутри этой директории располагался файл с конфигурацией нашего приложения. Изменим playbook под это: ```yaml --- - name: "ping hosts" hosts: "all" tags: [check] tasks: - ping: - name: "ensure direcories" hosts: "all" tags: [dirs] tasks: - file: state: directory path: "/opt/test" - name: "render config file" hosts: "all" tags: [configs] tasks: - copy: dest: "/opt/test/config.yaml" src: "config.yaml" ``` ```yaml # config.yaml --- app: name: project env: prod secret: s3cr37 ``` После проигрывания playbook, файл будет лежать по пути copy.dest на каждом хосте, для которого был сыгран play “render config file”. Но хранить секреты в явновном виде - очевидно плохая идея; к счастью Ansible умеет шифровать и расшифровывать секреты, для этого существует утилита `ansible-vault`. Для начала, нам необходим ключ шифрования, в качестве примера созданим простой ключ: ```bash $ openssl rand -hex 8 > .vault ``` и зашифруем с его помощью секреты в `config.yaml`: ```bash $ ANSIBLE_VAULT_PASSWORD_FILE=.vault cat config.yaml | yq '.app.secret' | tr -d "\n" | ansible-vault encrypt Encryption successful $ANSIBLE_VAULT;1.1;AES256 36633139366237646539356365376435353066363637663963353737333561386461643834663861 3063303333393837633135636430313236653432336333640a623036613933373561313834626437 36303133326330613030646437366534353866653966373132653363306539346431313962336162 3333663466303139360a663837643633353837613961376535663837306161663232373137383030 3236 ``` Все, начиная с `$ANSIBLE_VAULT` включительно - зашифрованный ключ, который достаточно вставить в `config.yaml` через указание `!vault |`: ```yaml --- app: name: project env: prod secret: !vault | $ANSIBLE_VAULT;1.1;AES256 36633139366237646539356365376435353066363637663963353737333561386461643834663861 3063303333393837633135636430313236653432336333640a623036613933373561313834626437 36303133326330613030646437366534353866653966373132653363306539346431313962336162 3333663466303139360a663837643633353837613961376535663837306161663232373137383030 3236 ``` Сыграем наш playbook, и зайдем проверить что получилось: ```bash [root@cli1.domain.tech test]$ cat config.yaml --- app: name: project env: prod secret: !vault | $ANSIBLE_VAULT;1.1;AES256 36633139366237646539356365376435353066363637663963353737333561386461643834663861 3063303333393837633135636430313236653432336333640a623036613933373561313834626437 36303133326330613030646437366534353866653966373132653363306539346431313962336162 3333663466303139360a663837643633353837613961376535663837306161663232373137383030 3236 ``` Секрет остался зашифрованным - логично, ведь нужно явно указать в playbook о необходимости его расшифровки. Обновим наш playbook: ```yaml --- - name: "ping hosts" hosts: "all" tags: [check] tasks: - ping: - name: "ensure direcories" hosts: "all" tags: [dirs] tasks: - file: state: directory path: "/opt/test" - name: "read config file template and render" hosts: "all" tags: [configs] tasks: - name: "config | read config file template" include_vars: file: config.yaml name: config_yml - name: "config | render config file" copy: dest: "/opt/test/config.yaml" content: "---\n{{ config_yml | string | from_yaml | to_nice_yaml(indent=2) }}" ``` И сыграем его, не забыв указать путь по ключа в переменной окружения `ANSIBLE_VAULT_PASSWORD_FILE`, которая указывает на файл ключа, с помощью которого, секрет будет расшифрован: ```bash $ ANSIBLE_VAULT_PASSWORD_FILE=.vault ansible-playbook ping.yaml -i project.hosts -l cli_preprod -b -t=configs PLAY [ping hosts] ************************************************************************************************* PLAY [ensure direcories] ****************************************************************************************** PLAY [read config file template and render] *********************************************************************** TASK [Gathering Facts] ******************************************************************************************** ok: [cli1.domain.tech] TASK [config | read config file template] ************************************************************************* ok: [cli1.domain.tech] TASK [config | render config file] ******************************************************************************** changed: [cli1.domain.tech] PLAY RECAP ******************************************************************************************************** cli1.domain.tech : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` и проверим результат: ```bash [root@cli1.domain.tech test]$ cat config.yaml --- app: env: prod name: project secret: s3cr37 ``` Готово - так, мы можем хранить секреты прямо в репозитории и расшифровывать их непосредственно при развертывании конфигурации приложения. # Итог Безусловно, здесь затронута только самая верхушка айсберга - ни слова про `gathering_facts`, `run_once`, `when`, `handlers` и тонну других полезных вещей, но это а) куда как лучше и правильнее описано в официальной документации и б) не нужно в рамках 101, ведь главной целью было дать лишь основные представления об Ansible и его применении для конечного разработчика. RTMF, folks!