Ansible: тестируем плейбуки (часть 1)

70f307920e014fca8bc019fc8df7719b.jpg

Думаю, любой системный администратор, использующий 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
│ └── host_vars
├── logs
├── roles
│ ├── common
│ │ ├── defaults
│ │ │ └── main.yml
│ │ ├── files
│ │ ├── handlers
│ │ │ └── main.yml
│ │ ├── tasks
│ │ │ ├── install_packages.yml
│ │ │ └── main.yml
│ │ ├── templates
│ │ └── vars
│ └── nginx
│ ├── defaults
│ ├── files
│ ├── handlers
│ │ └── main.yml
│ ├── tasks
│ │ ├── configure.yml
│ │ ├── install.yml
│ │ └── main.yml
│ ├── templates
│ │ └── nginx.conf.j2
│ └── vars
├── site.yml
├── Vagrantfile
└── vars
└── 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
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── tasks
│ ├── install_packages.yml
│ └── main.yml
├── templates
└── 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 (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 (0m0.93s).

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

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

Автор: DevOps-администратор Southbridge — Виктор Батуев.

© Habrahabr.ru