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.

Revisions

  1. tony-sol renamed this gist May 12, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. tony-sol created this gist Mar 31, 2022.
    538 changes: 538 additions & 0 deletions ansible-101.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,538 @@
    # 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
    [[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`:

    ```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
    [[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/
    ```

    Вывод - нам нужна [эскалация привилегий](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
    [[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:

    ```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
    [[email protected] test]$ cat config.yaml
    ---
    app:
    env: prod
    name: project
    secret: s3cr37
    ```

    Готово - так, мы можем хранить секреты прямо в репозитории и расшифровывать их непосредственно при развертывании конфигурации приложения.

    # Итог

    Безусловно, здесь затронута только самая верхушка айсберга - ни слова про `gathering_facts`, `run_once`, `when`, `handlers` и тонну других полезных вещей, но это а) куда как лучше и правильнее описано в официальной документации и б) не нужно в рамках 101, ведь главной целью было дать лишь основные представления об Ansible и его применении для конечного разработчика.

    RTMF, folks!