Думаю, любой системный администратор, использующий Ansible для управления своим зоопарком серверов задавался вопросом о проверке корректности описания конфигурации своих серверов. Как же не бояться вносить изменения в конфигурации серверов?
В серии статей, посвященных DevOps, мы расскажем об этом.



Несколько условий, при котором мы будем выполнять тестирование конфигураций:

1. Вся конфигурация хранится в git-репозитории.
2. Jenkins (CI-сервис) периодически опрашивает репозиторий с нашими ролями/плейбуками на предмет внесённых изменений.
3. При появлении изменений Jenkins запускает сборку конфигурации и покрывает её тестами. Тесты состоят из двух этапов:
3.1 Test-kitchen берёт обновленный код из репозитория, запускает полностью свежие docker-контейнер, заливает в них обновлённые плейбуки из репозитория и запускает ansible локально, в docker-контейнере.
3.2 Если первый этап прошёл успешно, в docker-контейнере запускается serverspec и проверяет, корректно ли встала новая конфигурация.
4. Если в test-kitchen все тесты прошли успешно, то Jenkins инициирует заливку новой конфигурации.

Конечно, можно запускать каждый плейбук/роль в Vagrant (благо, там есть такая крутая штука как provisioning), проверять, что конфигурация соотвествует ожидаемой, но каждый раз для теста новой или изменённой конфигурации выполнять столько ручных действий — сомнительное удовольствие. Зачем? Ведь можно всё автоматизировать. Для этого к нам приходят такие замечательные инструменты как Test-kitchen, Serverspec и, конечно же Docker.

Давайте для начала рассмотрим, как нам тестировать код в Test-kitchen на примере пары сферических ролей в вакууме.

Ansible.



Ansible я собирал последний, самый свежий из исходников. Предпочёл собирать руками. (кому лень — можно воспользоваться Omnibus-ansible)
git clone git://github.com/ansible/ansible.git --recursive
cd ./ansible


Собираем и устанавливаем deb-пакет (тестировать плейбуки будем на Debian).
make deb
dpkg -i deb-build/unstable/ansible_2.1.0-0.git201604031531.d358a22.devel~unstable_all.deb


Ansible встал, проверим:
ansible --version
ansible 2.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides


Отлично! Значит, пора перейти к делу.

Теперь нам необходимо создать git-репозиторий.
mkdir /srv/ansible && cd /srv/ansible
git init
mkdir base && cd base # Создаём папку проекта с конфигурацией


Архитектура репозитория примерно следующая:
+-- ansible.cfg
+-- inventory
¦   +-- group_vars
¦   +-- hosts.ini
¦   L-- host_vars
+-- logs
+-- roles
¦   +-- common
¦   ¦   +-- defaults
¦   ¦   ¦   L-- main.yml
¦   ¦   +-- files
¦   ¦   +-- handlers
¦   ¦   ¦   L-- main.yml
¦   ¦   +-- tasks
¦   ¦   ¦   +-- install_packages.yml
¦   ¦   ¦   L-- main.yml
¦   ¦   +-- templates
¦   ¦   L-- vars
¦   L-- nginx
¦       +-- defaults
¦       +-- files
¦       +-- handlers
¦       ¦   L-- main.yml
¦       +-- tasks
¦       ¦   +-- configure.yml
¦       ¦   +-- install.yml
¦       ¦   L-- main.yml
¦       +-- templates
¦       ¦   L-- nginx.conf.j2
¦       L-- vars
+-- site.yml
+-- Vagrantfile
L-- vars
    L-- nginx.yml


Конфигурационный файл по-умолчанию изменять не будем, внесём лишь свои правки в конфигурационный файл проекта.

ansible.cfg:
[defaults]
roles_path	    = ./roles/			        # Папка с ролями
retry_files_enabled = False                             # Отключаем retry-файлы в случае неудачного выполнения таска
become		    = yes					# Параметр эквивалентен вызову sudo
log_path	    = ./logs/ansible.log			# логи
inventory	    = ./inventory/				# Путь к inventory-файлам.


Далее нам нужен inventory-файл, где нужно указать список хостов, с которыми мы будем работать.
mkdir inventory
cd invetory
mkdir host_vars
mkdir group_vars


Файл invetory:
127.0.0.1 ansible_connection=local


Здесь перечислены все хосты, которыми будет управлять ansible.
host_vars — папка, где будут храниться переменные, которые могут отличаться от базовых значений в роли.
Пример: в ansible может быть полезен jinja2-шаблонизатор при работе с файлами и конфигами.
У нас есть шаблон resolv.conf templates/resolv.conf.j2:
nameserver {{ nameserver }}


В файле переменных по-умолчанию (roles/common/defaults/main.yml) указано:
nameserver: 8.8.8.8


Но на хост 1.1.2.2 нам нужно залить resolv.conf с другим значением nameserver.
Проворачиваем это через host_vars/1.1.2.2.yml:
nameserver: 8.8.4.4


В этом случае, при выполнении плейбука на все хосты зальётся стандартный resolv.conf (со значением 8.8.8.8), а на хост 1.1.2.2 — со значением 8.8.4.4.
Подробнее об этом можно почитать в документации Ansible

common-role



Это роль, которая выполняет стандартные задачи, которые должны выполняться на всех хостах. Установка каких-нибудь пакетов, к примеру, заведение пользователей, etc.
Структуру я немного описал выше. Пройдёмся по подробностям.

Структура роли:
./roles/common/
+-- defaults
¦   L-- main.yml
+-- files
+-- handlers
¦   L-- main.yml
+-- tasks
¦   +-- install_packages.yml
¦   L-- main.yml
+-- templates
L-- vars


В файле roles/common/defaults/main.yml указаны переменные, задаваемые по-умолчанию.
---
deb_packages:
  - curl
  - fail2ban
  - git
  - vim 

rh_packages:
  - curl
  - epel-release
  - git
  - vim


В папке files лежат файлы, которые должны быть скопированы на удалённый хост.
В папке tasks перечислены все задачи, которые должны быть выполнены при присвоении хосту роли.
roles/common/tasks/
+-- install_packages.yml
+-- main.yml


roles/common/tasks/install_packages.yml
---
- name: installing Debian/Ubuntu pkgs
  apt: pkg={{ item }} update_cache=yes
  with_items: "{{deb_packages}}"
  when: (ansible_os_family == "Debian")

- name: install RHEL/CentOS packages
  yum: pkg={{ item }}
  with_items: "{{rh_packages}}"
  when: (ansible_os_family == "RedHat")


Здесь использованы циклы with_items и when. Если дистрибутив семейства Debian — будут установлены пакеты из списка deb_packages с помощью модуля apt. Если дистрибутив семейства RedHat — будут установлены пакеты из списка rh_packages с помощью модуля yum.

roles/common/tasks/main.yml
---
- include: install_packages.yml


(Да, я очень люблю декомпозировать роли на отдельные файлы со своими задачами).

В файле main.yml просто инклудятся yaml-файлы, где описаны все задачи, описанные в папке tasks.

В папке templates лежат шаблоны в формате Jinja2 (выше был рассмотрен пример с resolv.conf).

В папке handlers перечислены действия, которые могут совершать после выполнения каких-либо тасков. Пример: имеем кусок таска:
- name: installing Debian packages
  apt: pkg=fail2ban update_cache=yes
  when: (ansible_os_family == "Debian")
  notify:
    - restart fail2ban


и хэндлер roles/common/handlers/main.yml:
---
- name restart fail2ban
  service: name=fail2ban state=restarted


В этом случае после выполнении таска apt: pkg=fail2ban update_cache=yes запустится задача-хэндлер restart fail2ban. Другими словами fail2ban перезапуститься сразу, как только будет установлен. В противном случае, если fail2ban в нашей системе уже установлен, то нотификация и запуск хэндлера будут проигнорированы)

В папке vars можно указать переменные, которые должны использоваться не по-умоланию.
/vars/common.yml
---
deb_packages:
  - curl
  - fail2ban
  - vim
  - git
  - htop
  - atop
  - python-pycurl
  - sudo

rh_packages:
  - curl
  - epel-release
  - vim
  - git
  - fail2ban
  - htop
  - atop
  - python-pycurl
  - sudo


Test-kitchen + serverspec.



Ресурсы, которые были использованы:

serverspec.org/resource_types.html

github.com/test-kitchen/test-kitchen
github.com/portertech/kitchen-docker
github.com/neillturner/kitchen-verifier-serverspec
github.com/neillturner/kitchen-ansible
github.com/neillturner/omnibus-ansible

Test-kitchen — это инструмент для интеграционного тестирования. Он подготавливает среду для тестирования, позволяет быстро запустить контейнер/виртуальную машину и протестировать плейбук/роль.
Умеет работать с vagrant. но мы в качестве провайдера будем использовать docker.
Устанавливается как гем, можно использовать gem install test-kitchen, но я предпочитаю использовать bundler. Для этого необходимо в папке с проектом создать Gemfile и прописать в нём все гемы и их версии.
source 'https://rubygems.org'

gem 'net-ssh','~> 2.9'
gem 'serverspec'
gem 'test-kitchen'
gem 'kitchen-docker'
gem 'kitchen-ansible'
gem 'kitchen-verifier-serverspec'


Очень важно указать версию гема net-ssh, т. к. с более новой версией test-kitchen, вероятно, работать не будет.
Теперь нужно выполнить bundle install и подождать пока все гемы с зависимостями установятся.
В папке с проектом делаем kitchen init. В папке появится файл .kitchen.yml, который необходимо привести примерно к следующему виду:
---
driver:
  name: docker

provisioner:
  name: ansible_playbook
  hosts: localhost
  require_chef_for_busser: false
  require_ansible_omnibus: true
  use_sudo: true

platforms:
  - name: ubuntu-14.04
    driver_config:
      image: vbatuev/ubuntu-rvm
  - name: debian-8
    driver_config:
      image: vbatuev/debian-rvm

verifier:
  name: serverspec
  additional_serverspec_command: source $HOME/.rvm/scripts/rvm

suites:
  - name: Common
    provisioner:
      name: ansible_playbook
      playbook: test/integration/default.yml
    verifier:
      patterns:
      - roles/common/spec/common_spec.rb


На этом этапе у меня возникли сложности с запуском serverspec в контейнере, поэтому мне пришлось применить небольшой workaround.
Все образы собраны мной и выложены в dockerhub, в каждом образе заведён пользователь kitchen, из под которого выполняются тесты, и установлен rvm с версией ruby 2.3.
Параметр additional_serverspec_command указывает, что мы будем использовать rvm. Это способ, при котором не нужны танцы с бубном вокруг версий ruby в стандартных репозиториях, зависимостями гемов и запуском rspec. В противном случае, с запуском serverspec-тестов придётся попотеть.
Дело в том, что kitchen-verifier-serverspec ещё довольно сыроват. Пока писал статью — пришлось отправить несколько баг-репортов и PR автору.

В секции suites мы указываем плейбук с ролью, которые будем проверять.
playbook: test/integration/default.yml
---
- hosts: localhost
  sudo: yes
  roles:
    - common


и patterns для теста serverspec.
    verifier:
      patterns:
      - roles/common/spec/common_spec.rb


Как выглядит тест:
common_spec.rb
require '/tmp/kitchen/roles/common/spec/spec_helper.rb'

describe package( 'curl' ) do
    it { should be_installed }
end


Здесь также очень важно указать в заголовке require именно такой путь. Иначе, он не найдёт и ничего работать не будет.

spec_helper.rb
require 'serverspec'
set :backend, :exec


Полный список того, что serverspec умеет проверять указан тут.

Команды:

kitchen test — запускает все этапы тестов.
kitchen converge — запускает плейбук в контейнере.
kitchen verify — запускает serverspec.

Результаты должны быть примерно такие:

При выполнении плейбука:
       Going to invoke ansible-playbook with: ANSIBLE_ROLES_PATH=/tmp/kitchen/roles sudo -Es  ansible-playbook -i /tmp/kitchen/hosts  -c local -M /tmp/kitchen/modules         /tmp/kitchen/default.yml
       [WARNING]: log file at ./logs/ansible.log is not writeable and we cannot create it, aborting
       
       [DEPRECATION WARNING]: Instead of sudo/sudo_user, use become/become_user and 
       make sure become_method is 'sudo' (default). This feature will be removed in a 
       future release. Deprecation warnings can be disabled by setting 
       deprecation_warnings=False in ansible.cfg.
       
       PLAY ***************************************************************************
       
       TASK [setup] *******************************************************************
       ok: [localhost]
       
       TASK [common : include] ********************************************************
       included: /tmp/kitchen/roles/common/tasks/install_packages.yml for localhost
       
       TASK [common : install {{ item }} pkgs] ****************************************
       changed: [localhost] => (item=[u'curl', u'fail2ban', u'git', u'vim'])
       
       TASK [common : install {{ item }} packages] ************************************
       skipping: [localhost] => (item=[]) 
       
       TASK [common : include] ********************************************************
       included: /tmp/kitchen/roles/common/tasks/create_users.yml for localhost
       
       TASK [common : Create admin users] *********************************************
       
       TASK [common : include] ********************************************************
       included: /tmp/kitchen/roles/common/tasks/delete_users.yml for localhost
       
       TASK [common : Delete users] ***************************************************
       ok: [localhost] => (item={u'name': u'testuser'})
       
       RUNNING HANDLER [common : start fail2ban] **************************************
       changed: [localhost]
       
       PLAY RECAP *********************************************************************
       localhost                  : ok=7    changed=2    unreachable=0    failed=0   
       
       Finished converging <Common-ubuntu-1404> (3m58.17s).


При запуске serverspec:
       Running Serverspec
       
       Package "curl"
         should be installed
       
       Package "vim"
         should be installed
       
       Package "fail2ban"
         should be installed
       
       Package "git"
         should be installed
       
       Finished in 0.12682 seconds (files took 0.40257 seconds to load)
       4 examples, 0 failures
       
       Finished verifying <Common-ubuntu-1404> (0m0.93s).


Если всё прошло успешно — значит, мы только что подготовили первый этап для тестирования плейбуков и ролей ansible. В следующей части мы рассмотрим, как добавить ещё больше автоматизации для тестирования инфраструктурного кода Ansible с помощью такого замечательного инструмента как Jenkins.

А как вы проверяете свои плейбуки?

Автор: DevOps-администратор Southbridge — Виктор Батуев.
Поделиться с друзьями
-->

Комментарии (16)


  1. amarao
    22.06.2016 10:43
    +5

    У меня есть один идейный вопрос: почему после няшного питонового ансибла мы начинаем адски bundler exec ruby was here? Фактически, все рубёвые штуки очень любят юзать ruby/erb синтаксис, что сильно увеличивает объём того, что нужно знать. И это ради тестов?

    Вообще, для ансибла я видел тесты, написанные на самом ансибле. Выглядят как отдельная playbook'а, состоящая из набора утверждений с fail (и условиями fail).

    Вот иметь какой-то слой интеграции между ansible и контейнерами — было бы мило.


    1. welcomerooot
      22.06.2016 11:26

      > Фактически, все рубёвые штуки очень любят юзать ruby/erb синтаксис, что сильно увеличивает объём того, что нужно знать. И это ради тестов?
      Возможно, потому что serverspec оказался первым попавшимся инструментом для тестирования.

      > Вообще, для ансибла я видел тесты, написанные на самом ансибле. Выглядят как отдельная playbook'а, состоящая из набора утверждений с fail (и условиями fail)
      О, а можете поделиться? Я видел только тесты плейбуков на python'е. Но это выглядело как-то не очень.

      > Вот иметь какой-то слой интеграции между ansible и контейнерами — было бы мило.
      Не совсем понял, о чём идёт речь. У ansible есть connection-модуль для docker (немного кривоватый, правда), или Вы о другом?


      1. amarao
        22.06.2016 11:35
        +4

        У ансибла обновилась документация, вот сходу: http://docs.ansible.com/ansible/test_strategies.html

        Вот ещё нашлось: https://github.com/nylas/ansible-test

        В живую я вопрос тестов сам не щупал, хотя очень интересно было бы.

        По второму вопросу: речь про то, чтобы иметь какой-то вменяемый формат для описания «что нужно для теста».


        1. welcomerooot
          22.06.2016 11:38

          О, благодарю за ansible-test, это интересно.


  1. shadowalone
    22.06.2016 11:25
    -6

    Какой-то детский сад прям, а не DevOps.
    И еще, почему не упомянули про ansible-galaxy, ну хотя бы в аспекте создания проекта (папок), например:
    # ansible-galaxy init common -p roles


  1. 1it
    22.06.2016 12:27
    +4

    Обычно во взрослых компаниях так или иначе использующих методологию DevOps настраивают несколько окружений. И как правило тестируют новый код на соответсвующем окружении (DEV например).
    Вообще, мне не очень понятна идея автоматизированного тестирования плейбуков, на мой взгляд нужно в первую очередь четко понимать что в них писать и для чего (ну и желательно как). Т.е. проверять их в любом случае нужно глазами, а потом уже пробовать запускать на DEV-окружении. Плюс есть же еще Dry run. А то получается сам накодил, сам запустил автотесты, сам себе вернул на доработку.


    1. questor
      22.06.2016 12:57

      Согласен с тем, что вызывает недоумение то, что автор декларирует devops — и тут же описывает единый inventory, когда даже в документации рекомендуемая структура содержит production и staging.


  1. questor
    22.06.2016 12:53

    Статью автора могу только приветствовать, он по касательной затрагивает тему, которая мне в последнее время интересна — ansible best practice.

    Вот очень бы хотелось увидеть чью-то готовую статью на эту тему, потому что самому писать пытаясь обобщить собственный опыт времени не хватает: это не меньше месяца-двух точно займёт. (Никто не хочет поделиться своими наработками, а?)

    На хабре было несколько вводных статей по ансиблу, вроде и хостеры пишут (инфобокс, селектел) — а всё равно статьи вышли уровня для новичков. Навскидку: мы же храним ansible скрипты в git'е, значит конфиги хостов должны либо лежать вне git, либо уж хотя бы пароли в открытом виде не содержать и закрытые ключи ssh.

    Собирать информацию по бест практис приходится по крохам.

    Вот в текущей статье автор опять зачем разжёвывал структуру плейбуков, полстатьи можно смело под нож урезать и сосредоточиться именно на тестировании ролей — ан нет, повторяет документацию.

    Например, явно хорошей практикой (на мой взгляд) является "одна роль — одна переменная (массив)", в то же время и в текущей статье я не вижу hash_behaviour = merge в конфиге и вижу на гитхабе в репозиториях простыни глобальных переменных.

    Тестирование плейбуков у нас делается по-старинке, вручную: создай пустую виртуальную машину и тестируйся по самое не хочу откатываясь к контрольным точкам. Статья же дала импульс поискать варианты решения на базе teamcity (у нас на него лицензии куплены, корпоративный стандарт).


    1. 1it
      22.06.2016 13:04
      +1

      Навскидку: мы же храним ansible скрипты в git'е, значит конфиги хостов должны либо лежать вне git, либо уж хотя бы пароли в открытом виде не содержать и закрытые ключи ssh.

      Можно (и нужно) всё хранить в git даже пароли если использовать (ansible-vault). Ну кроме файлов приватных ключей наверное, хотя их тоже можно хранить в виде зашифрованных переменных.


  1. aardvarkx1
    22.06.2016 13:44
    +3

    Вместо serverspec мне кажется лучше использовать testinfra для ansible.


    1. Corpsee
      22.06.2016 14:49

      Спасибо за testinfra, давно такую штуку искал.


  1. thunderspb
    22.06.2016 14:19
    +1

    Занимаюсь примерно такой же фигней, только столкнулся с тем, что kitchen-ansible не раскрывает переменные внутри, например, group_vars/all файлах. Пришлось свой велосипед написать :(


  1. le9i0nx
    22.06.2016 16:21
    +2

    использую свой велосипед на python скрипте github.com/le9i0nx/ansible-role-test/blob/master/bin/virt.py
    Travis файл github.com/le9i0nx/ansible-syncthing/blob/master/.travis.yml

    скрипт читает meta для получения списка поддерживаемых ос и создает докер контейнеры с настроенным ключевым доступом.
    потом запускается ansible.
    тест считается удачным если роль отработала без ошибок во всех контейнерах.
    проблемой для меня сейчас является только скорость выполнения такого теста (6мин +-2 мин)


  1. bborysenko
    23.06.2016 12:47
    +1

    Поделюсь своим небольшим опытом. Путь был примерно следующий:

    — Vagrantfile который со временем вырос во что-то стремное github.com/owox-ansible-roles/ansible-role-php/blob/master/Vagrantfile + github.com/owox-ansible-roles/ansible-role-php/blob/master/Makefile
    — Со временем пришло осознание что пора завязывать с собственными велосипедами, и под руку попался тот самый github.com/neillturner/kitchen-ansible. Мне нахватало пару мелочей и полез смотреть внутрь как оно там все устроено. PR быстро залетели в мастер и в принципе с этим можно было жить дальше. Но вот тянуть за собой Ruby в проект очень и очень не хотелось.
    — Потом очень удачно где-то в рассылках наткнулся на github.com/metacloud/molecule. Грубо говоря, это обертка над Vagrant написанная на Python очень близко копирующая логику Test Kithcen. И вот эта штуковина реально прижилась. Кстати недавно впилили поддержку Docker, но сам еще не смотрел. Сейчас Ansible-ом накатываем реальные сервера, и как по мне не совсем коректно тестировать результат в контейнере. Хотя есть мысли подменять часть задач исходя из github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/facts.py#L2834.

    По поводу serverspec, в конце концов ушли от него и начали юзать тот же Ansible для тестирования себя любимого. Правда местами все же юзаем github.com/sstephenson/bats.

    Небольшое замечание по оформлению — у вас практически везде сьехала разметка кода. Это я про структуру репозитория, ну и про YAML-ки Test-kitchen. Поправьте пожалуйста, а то глаз режет.


  1. akamensky
    24.06.2016 11:25

    Поделюсь немного нашим опытом:

    Мы используем Ansible для настройки новых VM на AWS. Для этого мы написали Python скрипт который выполняется каждую ночь:

    — Скрипт сначала обновляет playbook'и из dev ветки, затем читает определенную директорию где лежат «задачи». Каждая задача это — список AMI ID и роль для хоста.
    — Затем для каждой задачи этот скрипт запускает чистую машину с указанным AMI и через AWS API получает информацию о машине.
    — Из полученной информации собирается inventory и в отдельном потоке начинается выполнение playbook на этой виртуалке. Соответственно STDOUT, STDERR и код возврата сохраняются в результатах выполнения
    — Если все выполнилось без ошибок (определяется по коду возврата), то машина останавливается и удаляется. Если же произошла какая-либо ошибка, то машина просто останавливается и разработчики на следующий день могут залогиниться и посмотреть почему произошла ошибка.
    — Когда все потоки закончили выполнятся, результаты отправляются по емейлу в виде билд-матрицы (по горизонтали различные AMI/системы, по вертикали — роли)

    Было бы очень интересно узнать как такое решение можно реализовать на Jenkins.


    1. welcomerooot
      24.06.2016 11:36
      +2

      Ну, вы же всё уже описали :) Создаём задачу в Jenkins, описываем шаги сборки, шлём e-mail по окончанию. Jenkins определяет статус джоба по коду возврата.