Ansible с нуля: установка, inventory и первый playbook

опубликовано
читать
≈ 7 мин
автор
среда и версия
Debian 12, ansible-core 2.14, OpenSSH 9.2
содержание пройти тест DevOps

Ansible настраивает серверы по SSH: агент на них ставить не нужно, хватает Python. Список машин лежит в inventory, а шаги настройки — в playbook на YAML. Главное свойство — повторный запуск: playbook описывает нужное состояние, поэтому второй прогон на уже настроенном сервере ничего не меняет. Ниже — путь от установки до playbook с nginx на двух серверах Debian 12, с живым выводом и разбором ошибок.

DevOps / 01

Второй запуск ничего не меняет

Второй запуск ничего не меняетОдин и тот же playbook запущен дважды на web1. Первый запуск: сбор фактов ok, установка nginx changed, конфигурация changed, запуск nginx changed, обработчик перезагрузки changed, итог changed=4. Второй запуск: все четыре задачи ok, обработчик не вызван, итог changed=0.задачапервый запусквторой запускGathering FactsokokУстановить nginxchangedokПоложить конфигурацию страницы статусаchangedokЗапустить nginx и включить автозапускchangedokобработчик «Перезагрузить nginx»changedне вызванPLAY RECAPchanged=4changed=0
Задачи сравнивают нужное состояние с текущим и меняют только разницу. Поэтому на уже настроенном сервере второй прогон отвечает ok, а обработчик перезагрузки не вызывается.

Установка

Ansible ставят только на управляющую машину — ту, с которой запускаешь команды. В Debian и Ubuntu он есть в репозитории:

sudo apt install ansible
ansible --version | head -1
ansible [core 2.14.18]

На серверах нужны три вещи: SSH, Python 3 и пользователь, которому разрешён sudo. В примере это пользователь deploy на машинах web1 и web2, а управляющая машина входит к нему по ключу. Как настроить вход по ключу, разобрано в материале о подключении по SSH.

Inventory: список серверов

Inventory — файл со списком машин и групп. В квадратных скобках имя группы, в строке — имя машины и её адрес, а секция [web:vars] задаёт переменные для всей группы:

[web]
web1 ansible_host=203.0.113.11
web2 ansible_host=203.0.113.12

[web:vars]
ansible_user=deploy

Первое подключение упирается в ключи хостов, как и у обычного ssh. В терминале ssh спросит про ключ каждой машины, а без терминала, например в CI, подключение сразу падает:

ansible web -i inventory.ini -m ping
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: Host key verification failed.",
    "unreachable": true
}
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: Host key verification failed.",
    "unreachable": true
}

Ключи можно собрать заранее командой ssh-keyscan -H 203.0.113.11 203.0.113.12 >> ~/.ssh/known_hosts. Она доверяет тому, что ответило по сети, поэтому отпечатки стоит сверить с теми, что показывают сами серверы. После этого модуль ping проверяет связь и Python на каждой машине:

ansible web -i inventory.ini -m ping
web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
web2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Разовые команды

Без playbook Ansible выполняет одну команду сразу на всей группе. Ключ -a передаёт её модулю command:

ansible web -i inventory.ini -a "hostname"
web1 | CHANGED | rc=0 >>
web1
web2 | CHANGED | rc=0 >>
web2

CHANGED здесь не значит, что что-то сломали. Модули command и shell не знают, что делает запущенная команда, поэтому всегда отвечают «изменено». Для настройки серверов берут модули, которые понимают состояние: apt, template, service, lineinfile.

Первый playbook

Playbook описывает, что должно быть на серверах. Этот ставит nginx, кладёт конфигурацию страницы статуса и следит, чтобы nginx работал:

- name: Настроить веб-серверы
  hosts: web
  become: true

  tasks:
    - name: Установить nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Положить конфигурацию страницы статуса
      ansible.builtin.template:
        src: templates/status.conf.j2
        dest: /etc/nginx/conf.d/status.conf
        mode: "0644"
      notify: Перезагрузить nginx

    - name: Запустить nginx и включить автозапуск
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Перезагрузить nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

hosts: web — на какой группе работать. become: true выполняет задачи через sudo. state: present у apt значит «пакет должен стоять», а не «поставь его ещё раз». Шаблон templates/status.conf.j2 — обычный файл конфигурации, в который Ansible подставляет переменные:

server {
    listen 8080;
    location /status {
        return 200 "ok {{ inventory_hostname }}\n";
    }
}

notify вызывает обработчик из секции handlers, только если задача что-то изменила: nginx перезагружается после правки конфигурации, а не при каждом запуске. Первый прогон:

ansible-playbook -i inventory.ini site.yml
PLAY [Настроить веб-серверы] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [web2]
ok: [web1]

TASK [Установить nginx] ********************************************************
changed: [web1]
changed: [web2]

TASK [Положить конфигурацию страницы статуса] **********************************
changed: [web2]
changed: [web1]

TASK [Запустить nginx и включить автозапуск] ***********************************
changed: [web2]
changed: [web1]

RUNNING HANDLER [Перезагрузить nginx] ******************************************
changed: [web1]
changed: [web2]

PLAY RECAP *********************************************************************
web1                       : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
web2                       : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Оба сервера отвечают со своим именем, которое подставил шаблон:

curl -s http://203.0.113.11:8080/status
curl -s http://203.0.113.12:8080/status
ok web1
ok web2

Повторный запуск

Тот же playbook второй раз:

ansible-playbook -i inventory.ini site.yml
PLAY [Настроить веб-серверы] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [web1]
ok: [web2]

TASK [Установить nginx] ********************************************************
ok: [web1]
ok: [web2]

TASK [Положить конфигурацию страницы статуса] **********************************
ok: [web1]
ok: [web2]

TASK [Запустить nginx и включить автозапуск] ***********************************
ok: [web1]
ok: [web2]

PLAY RECAP *********************************************************************
web1                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
web2                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

changed=0 и нет строки RUNNING HANDLER: всё уже в нужном состоянии, менять нечего. Это свойство называют идемпотентностью, и оно держится не на Ansible целиком, а на каждой задаче. Задача через shell его ломает. Вот такая строчка дописывает переменную в /etc/environment:

    - name: Дописать APP_ENV через shell
      ansible.builtin.shell: echo "APP_ENV=production" >> /etc/environment

Каждый прогон отвечает changed и добавляет строку заново. После двух запусков их две:

ansible web -i inventory.ini -a "grep -c APP_ENV /etc/environment"
web1 | CHANGED | rc=0 >>
2
web2 | CHANGED | rc=0 >>
2

Модуль lineinfile сначала ищет строку по регулярному выражению и меняет файл, только если её нет или она другая:

    - name: Задать APP_ENV
      ansible.builtin.lineinfile:
        path: /etc/environment
        regexp: "^APP_ENV="
        line: APP_ENV=production

Первый запуск с ним отвечает changed, второй — ok и changed=0 в итоге. Лишние копии, оставшиеся от shell, убирает тот же модуль с state=absent.

Главная мысль: хороший playbook можно запускать сколько угодно раз. Если второй прогон снова пишет changed, ищи задачу на shell или command.

Проверка без изменений: —check и —diff

Флаг --check прогоняет playbook, ничего не меняя, а --diff показывает, что именно поменялось бы. Допустим, в шаблоне текст ответа стал ok {{ inventory_hostname }} v2, а запуск ограничен одной машиной через --limit:

ansible-playbook -i inventory.ini site.yml --check --diff --limit web1
TASK [Положить конфигурацию страницы статуса] **********************************
--- before: /etc/nginx/conf.d/status.conf
+++ after: /home/dev/.ansible/tmp/ansible-local-5793xhrxqpg/tmpekmw5jz6/status.conf.j2
@@ -1,6 +1,6 @@
 server {
     listen 8080;
     location /status {
-        return 200 "ok web1\n";
+        return 200 "ok web1 v2\n";
     }
 }

changed: [web1]

Это отрывок вывода, остальные задачи ответили ok. На сервере ничего не поменялось: страница статуса отвечает по-старому.

curl -s http://203.0.113.11:8080/status
ok web1

Секреты: ansible-vault

Пароли в playbook не пишут открытым текстом. ansible-vault encrypt_string шифрует значение, и результат вставляют в переменные как есть:

ansible-vault encrypt_string --vault-password-file vault-pass.txt "app_password" --name db_password
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          64646338323163623763356437613564333539336433373030313032356534643265323837373235
          3162613437666134383630323564383665643634643961310a356161323234633664323938363461
          39373333343766643436623334613133316462303461363666366362326439333230373832653737
          3530396264643662350a623435653932396139396132333161396430643463346562306161383439
          3736

При запуске playbook тот же пароль передают через --vault-password-file vault-pass.txt или вводят по запросу с --ask-vault-pass. Файл с паролем от vault в репозиторий не кладут — его место в .gitignore.

Ошибки, на которых спотыкаются

Host key verification failed. Ключа сервера нет в ~/.ssh/known_hosts, а спросить некого. Собери ключи через ssh-keyscan и сверь отпечатки.

Permission denied (publickey,password). Ansible вошёл не тем пользователем или ключ не лежит на сервере:

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: admin@203.0.113.11: Permission denied (publickey,password).",
    "unreachable": true
}

Missing sudo password. Задаче нужен become, а пользователю на сервере sudo разрешён только с паролем:

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Пароль спрашивает флаг -K (--ask-become-pass), или правило sudo для этого пользователя меняют на вход без пароля.

couldn’t resolve module/action. Опечатка в имени модуля — здесь ansible.builtin.ap вместо ansible.builtin.apt:

ERROR! couldn't resolve module/action 'ansible.builtin.ap'. This often indicates a misspelling, missing collection, or incorrect module path.

The error appears to be in '/home/dev/infra/typo-module.yml': line 6, column 7, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  tasks:
    - name: Установить nginx
      ^ here

Указатель стоит на начале задачи, а не на самом модуле: ищи опечатку во всей задаче.

did not find expected key. Сломан отступ в YAML. Ansible показывает строку, где споткнулся разбор:

ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)

Syntax Error while loading YAML.
  did not find expected key

The error appears to be in '/home/dev/infra/typo-yaml.yml': line 9, column 8, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

        name: nginx
       state: present
       ^ here

No package matching. Модуль apt не нашёл пакет, обычно из-за опечатки в имени:

fatal: [web1]: FAILED! => {"changed": false, "msg": "No package matching 'ngnix' is available"}

Флаг -u ничего не меняет. Пользователь из inventory, ansible_user, важнее флага командной строки: у значений из командной строки самый низкий приоритет. Здесь -u admin проигнорирован:

ansible web1 -i inventory.ini -u admin -a whoami
web1 | CHANGED | rc=0 >>
deploy

Переопределить пользователя на один запуск можно через -e ansible_user=admin: у дополнительных переменных приоритет выше.

Частые вопросы

Чем Ansible лучше bash-скрипта по SSH

Bash-скрипт выполняет команды, Ansible приводит сервер к описанному состоянию. Скрипт, запущенный дважды, повторит всё дважды, а playbook на втором прогоне ничего не тронет. К тому же Ansible работает сразу с группой машин из inventory и показывает итог по каждой.

Нужно ли ставить Ansible на серверы

Нет. На серверах нужны SSH и Python, всё остальное Ansible привозит с собой на время выполнения задачи. Поэтому его и называют безагентным.

Что делать со службами на сервере

Модуль service запускает, останавливает и включает службы так же, как systemctl, только на всей группе сразу и с проверкой состояния.

Где потренироваться

Ansible — тема в карте навыков DevOps, рядом с Terraform и Kubernetes. Проверить себя можно в тесте по DevOps: 8 вопросов, баллы видны без регистрации.

Источники