Основы Ansible для новичка
Эта статья не замена https://docs.ansible.com/, а скорее 101 - база, которая поможет начать понимать основные принципы работы
Ansible - в каноничном определении, система управления конфигурациями.
Чуть более понятно и обобщенно - это инструмент с помощью которого можно автоматизировать практически любую задачу, которую можно выполнить “руками в консоли” - от подготовки сервера до деплоя конечного приложения.
Важно - Ansible не является “средством запуска shell-скриптов”, скрипты в целом для Ansible - считаются плохой практикой (крайне полезная статья на тему).
В отличии от Puppet, Ansible использует SSH для выполнения play на целевых хостах и не требует какого-либо агента. Из этого следует важная особенности - там где puppet-agent сам ходил в мастер, предоставлял факты о своем хосте и запрашивал изменения, в случае с Ansible - изменения не “проиграют”, пока явно их не “сыграть”.
Исходя из того что используется SSH, логично что машина, с которой будет пушиться конфигурация, должен иметь SSH доступ на управляемый хост и при этом даже не обязательно быть sudoers (правда это сильно ограничивает в том, что может быть исполнено).
Основным элементом Ansible является play - то что нужно “сыграть”, в какое состояние нужно привести какой-то аспект системы. Убедиться что на хосте существует директория, файл с содержимым, запущенный docker контейнер, etc. - это все play.
Play чаще всего не существует сам по себе - из него строится playbook и прямой перевод говорит сам за себя - “сборник, того что нужно сыграть” и если play это “логический” элемент, то playbook это его переложение “на файл”.
При этом сам play состоит из tasks и roles, и если с tasks относительно просто - это вызов указанного модуля, “функции” с параметрами и переменными, то roles можно воспринимать скорее как пакет - отдельная группа файлов с task’ами, handler’ами, переменными с четкой иерархией и структурой. У Ansible для этого есть даже свой “пакетный менеджер” - ansible-galaxy
, те кто хоть раз сталкивался с pip
сразу заметят сходство.
Важно - использование одновременно tasks и roles считается плохой практикой.
(крайне полезная статья на тему 2)
Но знать “что сыграть” не достаточно, нужно также знать “где сыграть”, в терминал Ansible это inventory - список хостов и их групп, на которых будет “сыгран” play.
Рассмотрим на конкретных примерах и навчнем с файла inventory. Файлы inventory поддерживаются в форматах ini и yaml | toml
[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
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 собран правильно, выполним первую команду:
$ 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.
$ 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"
}
$ 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"
}
$ 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"
}
$ 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:
# ping.yaml
---
- name: "our play" # имя для play
hosts: "all" # группа хостов/хост по умолчанию
tasks:
- name: "ping hosts" # имя task
ping: # использование встроенного модуля ping
И запустим на нашем inventory:
$ 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 на конечном хосте:
---
- name: "our playbook"
hosts: "all"
tasks:
- name: "ping hosts"
ping:
- name: "ensure direcories"
file:
state: directory
path: "/opt/test"
И запустим для группы cli_preprod:
$ 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 модом, за подробностями - в документацию) Если зайти на хост и проверить содержимое /opt, директории test там не окажется - check_mode наглядно.
[[email protected] 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
:
$ 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
- ansible-playbook
- ???
- ???
PROFIT!!!failed:
Что не так? Попробуем сделать это руками:
[[email protected] opt]$ mkdir test
mkdir: cannot create directory ‘test’: Permission denied
[[email protected] 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/
Вывод - нам нужна эскалация привилегий. Есть несоклько вариантов сделать это - можно указать become: true
в декларации конкретного task/play/ или сыграть playbook с флагом —become|-b
:
$ 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:
---
- 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 будут проигнорированы.
$ 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 под это:
---
- 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"
# config.yaml
---
app:
name: project
env: prod
secret: s3cr37
После проигрывания playbook, файл будет лежать по пути copy.dest на каждом хосте, для которого был сыгран play “render config file”.
Но хранить секреты в явновном виде - очевидно плохая идея; к счастью Ansible умеет шифровать и расшифровывать секреты, для этого существует утилита ansible-vault
.
Для начала, нам необходим ключ шифрования, в качестве примера созданим простой ключ:
$ openssl rand -hex 8 > .vault
и зашифруем с его помощью секреты в config.yaml
:
$ 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 |
:
---
app:
name: project
env: prod
secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
36633139366237646539356365376435353066363637663963353737333561386461643834663861
3063303333393837633135636430313236653432336333640a623036613933373561313834626437
36303133326330613030646437366534353866653966373132653363306539346431313962336162
3333663466303139360a663837643633353837613961376535663837306161663232373137383030
3236
Сыграем наш playbook, и зайдем проверить что получилось:
[[email protected] test]$ cat config.yaml
---
app:
name: project
env: prod
secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
36633139366237646539356365376435353066363637663963353737333561386461643834663861
3063303333393837633135636430313236653432336333640a623036613933373561313834626437
36303133326330613030646437366534353866653966373132653363306539346431313962336162
3333663466303139360a663837643633353837613961376535663837306161663232373137383030
3236
Секрет остался зашифрованным - логично, ведь нужно явно указать в playbook о необходимости его расшифровки. Обновим наш playbook:
---
- 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
, которая указывает на файл ключа, с помощью которого, секрет будет расшифрован:
$ 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
и проверим результат:
[[email protected] test]$ cat config.yaml
---
app:
env: prod
name: project
secret: s3cr37
Готово - так, мы можем хранить секреты прямо в репозитории и расшифровывать их непосредственно при развертывании конфигурации приложения.
Безусловно, здесь затронута только самая верхушка айсберга - ни слова про gathering_facts
, run_once
, when
, handlers
и тонну других полезных вещей, но это а) куда как лучше и правильнее описано в официальной документации и б) не нужно в рамках 101, ведь главной целью было дать лишь основные представления об Ansible и его применении для конечного разработчика.
RTMF, folks!