Skip to content

Instantly share code, notes, and snippets.

@tony-sol
Last active May 12, 2022 14:27
Show Gist options
  • Save tony-sol/e24d5ec750c427ee133a3afdc9be56cb to your computer and use it in GitHub Desktop.
Save tony-sol/e24d5ec750c427ee133a3afdc9be56cb to your computer and use it in GitHub Desktop.
Quickstart guide to ansible for those who have never used it before

Ansible

Основы Ansible для новичка

Эта статья не замена https://docs.ansible.com/, а скорее 101 - база, которая поможет начать понимать основные принципы работы

Что такое Ansible

Ansible - в каноничном определении, система управления конфигурациями.

Чуть более понятно и обобщенно - это инструмент с помощью которого можно автоматизировать практически любую задачу, которую можно выполнить “руками в консоли” - от подготовки сервера до деплоя конечного приложения.

Важно - Ansible не является “средством запуска shell-скриптов”, скрипты в целом для Ansible - считаются плохой практикой (крайне полезная статья на тему).

Как работает 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 считается плохой практикой.

(крайне полезная статья на тему 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
  1. ansible-playbook
  2. ???
  3. ???
  4. 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment