Pull to refresh
Southbridge
Обеспечиваем стабильную работу highload-проектов

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

Southbridge corporate blog
Tutorial


Думаю, любой системный администратор, использующий 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 <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 — Виктор Батуев.
Tags:
Hubs:
Total votes 24: ↑21 and ↓3 +18
Views 29K
Comments Comments 17

Information

Founded
Location
Россия
Website
southbridge.io
Employees
51–100 employees
Registered
Representative
Антон Скобин