Вы находитесь на странице: 1из 30

Сергей Печенко́

DevOps/SRE (Райффайзенбанк)

Ansible - это вам


не bash!
Serious Configuration Management
Об авторе
• За клавиатурой 28й год
• Оберегаю разные production’ы с 2013
• Изучаю и воплощаю DevOps-практики с 2014
• @tnt4brain `
• @pro_ansible/@ru_gitlab/@ru_logs/@ru_ldap/
• bantu.ru

2
TL;DR
● Готовим почву
● Об императивном стиле («bashsible»)
● Зачем писать код?
● Модули, плагины
● Шаблон модуля
● Рабочий модуль
● Рабочий плагин
● Используй Jinja, Люк!

3
NB!

4
Внутреннее устройство проекта Ansible
inventories/ inventories/
production/ environment1/
hosts inventory
group_vars/ group_vars/
group1.yml group1/
group2.yml service1.yml
host_vars/ service2.yml
hostname1.yml host_vars/
hostname2.yml hostname1.yml
8X........................8X playbooks/
library/ service1.yml
module_utils/ library/
filter_plugins/ module_utils/
roles/ *_plugins/
common/ roles/
monitoring/ common/
fooapp/ monitoring/
fooapp/

5
Настройки подключения
В секции [connection] файла ansible.cfg включаем опцию pipelining=True, eсли
используем переменные окружения - ANSIBLE_PIPELINING=True.

Ответ на вопрос “а почему не включено «искаропки»” - потому что конфликтует с


опцией команды sudo «RequireTTY», т.к. Python при pipelining «проваливается» в
интерактивный режим (см. ссылку⇝) .

Включение требует донастройки целевых хостов (как минимум в CentOS). Почему?

Потому что технические подробности жизни модуля (Ansible ≥2.1) выглядят вот так:

▪ модуль пакуется в .zip,


▪ Base64-ится,
▪ оборачивается в Python script,
▪ передаётся в целевую среду,
▪ подаётся на stdin интерпретатора Python,
▪ получает из stdin аргументы в виде JSON.

6
Тёмная сторона Силы
(bashsible)

7
Лёгкий, быстрый, неправильный способ писать плейбуки
---
- name: My kewl task
command: “/usr/bin/cowsay ‘this code smells, dude’”

- name: Another kewl one-shot task


shell: rm -rf /

8
Частые «причины» оправдания для bashsible
✓ — У меня нет на это времени!
✓ — Это что же, мне теперь всегда следить за конфигами и
менять шаблон?...
✓ — Мне это просто сказали поправить, я не эксплуатация
✓ — Там только две строчки заменить надо, остальное по
дефолту!
✓ — Мне надо засунуть всё в докер, там конфиг в образе уже
работающий!
✓ — Я не настоящий сварщик девопс, это задание делаю для
себя, чтобы научиться!
✓ — Да мне бы только тестовое сделать и сдать!

ЛОВЛЯ БАБОЧЕК ТОПОРОМ!!!


9
Путь джедая

10
Объектная модель Ansible
group/host playbook/play
group: all group: group2 host2 - hosts: group3
group_vars: group_vars: baz3: val5 become: yes
foo: val1 foo2: val7 baz4: val6 tasks:
bar: val2 bar2: val8 - name: “install”
yum:
host1:
name: “bar4”
baz: val3 group: group3 host3 state: installed
baz2: val4 group_vars: baz3: val7
foo7: val9 baz4: val8 - hosts: all
bar4: val10 become: yes
roles:
- role: role11
- role: role2
foo3: bar4

11
«За» и «против» написания своего кода для Ansible
1. В своём коде можно (и нужно!) 1. Необходимо:
реализовывать: a. думать, потом делать;
a. идемпотентность; b. уметь писать код на Python (или
b. платформонезависимость; на любом* другом знакомом
2. Код самого Ansible доступен для языке!);
изучения; c. поддерживать написанный код;
3. Из кода доступны вызовы других 2. «Астрологи объявили неделю
модулей и плагинов Ansible; самописных модулей для Ansible -
4. Написанный код, скорее всего, будет длительность входа в проект
переносим между версиями Ansible выросла!»;
(соглашения по вызову модулей 3. <younameit> ☺
неизменны);
5. <younameit> ☺

* Переносимость между платформами отсутствует

12
Модуль или плагин?
Модуль: Плагин:
▪ выполняется на управляемой ▪ выполняется на управляющей
системе (т.е. удалённо); системе (т.е. локально);
▪ но необязательно, см. «connection: ▪ но необязательно (см. template)
local» и много модулей, отвечающих
за конфигурирование “умных”
железок.

В общем случае модулю НУЖЕН Python В общем случае плагину НЕ НУЖЕН


на целевой системе. Python на целевой системе.

13
Размещение модуля и его вызов
inventories/ service1.yml:
environment1/ ---
inventory
- hosts: hostgroup1
group_vars/
become: yes
...
host_vars/ tasks:
... - name: “New module test”
playbooks/ да! new_module:
Модуль - сю
service1.yml
foo: bar
library/
few: “{{ baz }}”
new_module.py А сюда - все
module_utils/ text: ‘Привет, «Стачка»!’
вспомогательные
*_plugins/ Python-модули,
roles/ которые встречаются в
... Плагин - сюд
а! секции imports вашего
Ansible-модуля.

14
Минимальный модуль (1/2)
#!/usr/bin/python
#########################################
# joshuaconner's Ansible Module skeleton
# License: GNU GPL v3, just like Ansible
# https://gist.github.com/joshuaconner/
#########################################
try:
import MyKoolSupportModule # из module_utils
except ImportError, e:
print "failed=True msg='failed to import python module: %s'" % e
sys.exit(1)

15
Минимальный модуль (2/2)
def main():
changed = False
message = ''
module = AnsibleModule(
argument_spec = dict(
state = dict(default='present',
choices =['present', 'absent']),
name = dict(required=True),),
supports_check_mode=True)
# факты
facts = dict(‘key’: ‘value’)
# do_module_work(‘Рабочая часть модуля - делаем полезную работу’)
# Если получилось...
module.exit_json(changed=changed, msg=’Ура, смог!’, ansible_facts=facts)
# ...ну или не получилось
# module.fail_json(msg=’Увы, не смог’)
from ansible.module_utils.basic import *
main()

16
Реальный модуль (grafana_dashboard.py) (1/2)
# Copyright: (c) 2017, Thierry Sallé (@seuf), GPL v3.0+
#<imports, exceptions>
def main():
argument_spec = url_argument_spec()
del argument_spec['force']
del argument_spec['force_basic_auth']
del argument_spec['http_agent']
argument_spec.update(
state=dict(choices=['present', 'absent', 'export'],default='present'),
url=dict(aliases=['grafana_url'], required=True),
url_username=dict(aliases=['grafana_user'], default='admin'),
url_password=dict(aliases=['grafana_password'], default='admin',no_log=True),
grafana_api_key=dict(type='str', no_log=True),
org_id=dict(default=1, type='int'),
uid=dict(type='str'),
slug=dict(type='str'),
path=dict(type='str'),
overwrite=dict(type='bool', default=False),
message=dict(type='str'),)
#8X.....skipped.....X8
#grafana_[create,delete,export]_dashboard(module, module.params):

17
Реальный модуль (grafana_dashboard.py) (2/2) ⇝
module = AnsibleModule(argument_spec=argument_spec,supports_check_mode=False,
required_together=[['url_username', 'url_password', 'org_id']],
mutually_exclusive=[['grafana_user', 'grafana_api_key'],['uid', 'slug']],)
try:
if module.params['state'] == 'present':
result = grafana_create_dashboard(module, module.params)
elif module.params['state'] == 'absent':
result = grafana_delete_dashboard(module, module.params)
else:
result = grafana_export_dashboard(module, module.params)
except GrafanaAPIException as e:
module.fail_json(failed=True,msg="error : %s" % to_native(e)); return
except GrafanaMalformedJson as e:
module.fail_json(failed=True,msg="error : no slug parameter"); return
except GrafanaDeleteException as e:
module.fail_json(failed=True,msg="Can't delete dashboard : %s" % to_native(e)); return
except GrafanaExportException as e:
module.fail_json(failed=True,msg="Can't export dashboard : %s" % to_native(e)); return
module.exit_json(failed=False,**result); return
if __name__ == '__main__':
main()

18
Виды плагинов
● action_plugins - для написания своих actions, которые будут вызываться локально на управляющем хосте;
● callback_plugins - вывод сообщений о событиях (logstash, json, jabber, hipchat, grafana_annotations, debug,..);
● connection_plugins - способы подключения к управляемой среде (chroot, docker, kubectl, lxc, ssh, winrm, …..);
● lookup_plugins - для получения косвенных значений переменных (env, file, aws_*, consul_kv, url, redis, ..);
● filter_plugins - для отбора значений по критериям (ipaddr, json_query, network, urlsplit, to_yaml, fileglob, …….);
● cache_plugins - кэширование фактов о хостах (JSON, YaML, memcached, Redis, in-memory, pickle);
● shell_plugins - для выполнения команд в соответствующих оболочках (csh, fish, sh, powershell);
● netconf_plugins - для чтения конфигурации “умных” железок через разное (XML-RPC, HTTP API);
● strategy_plugins - для реализации custom-стратегий (linear, debug, free, host_pinned);
● terminal_plugins - для реализации протоколов обмена с “умными” железками;
● cliconf_plugins - для чтения конфигурации “умных” железок через CLI;
● test_plugins - для условных выражений в «when».

19
Вызов action-плагина
---
- hosts: group2
become: no
tasks:
- assert:
that:
- "my_param <= 100"
- "my_param >= 0"
msg: "'my_param' must be between 0 and 100"

20
Пример плагина (action_plugins/assert.py) (1/2)
# Copyright 2012, Dag Wieers <dag@wieers.com>
# This file is part of Ansible
#...
from ansible.errors import AnsibleError
from ansible.playbook.conditional import Conditional
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
TRANSFERS_FILES = False
_VALID_ARGS = frozenset(('fail_msg', 'msg', 'that'))
def run(self, tmp=None, task_vars=None):
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
if 'that' not in self._task.args:
raise AnsibleError('conditional required in "that" string')
fail_msg = None; success_msg = None;
fail_msg = self._task.args.get('fail_msg', self._task.args.get('msg'))
if fail_msg is None:
fail_msg = 'Assertion failed'

21
Пример плагина (action_plugins/assert.py) (2/2)
elif not isinstance(fail_msg, string_types):
raise AnsibleError('Incorrect type for fail_msg or msg, expected string and got
%s' % type(fail_msg))
success_msg = self._task.args.get('success_msg')
if success_msg is None:
success_msg = 'All assertions passed'
elif not isinstance(success_msg, string_types):
raise AnsibleError('Incorrect type for success_msg, expected string and got %s' %
type(success_msg))
thats = self._task.args['that']
if not isinstance(thats, list):
thats = [thats]
cond = Conditional(loader=self._loader); result['_ansible_verbose_always'] = True
for that in thats:
cond.when = [that]
test_result = cond.evaluate_conditional(templar=self._templar, all_vars=task_vars)
if not test_result:
result['failed'] = True; result['evaluated_to'] = test_result
result['assertion'] = that; result['msg'] = fail_msg; return result
result['changed'] = False; result['msg'] = success_msg; return result

22
Jinja2: используем Силу

23
Jinja2: аптечка первой помощи
Выражения - то, что непосредственно попадает в Операторы работают с внутренним контекстом Jinja2
вывод шаблона. и напрямую в вывод не попадают.
Заключаются в скобки «{{......}}»
Доступны константы в виде: {% set userlist = [{‘fname’:‘john’,‘lname’:
▪ строк, ‘doe’},{‘fname’:‘john’,‘lname’: ‘smith’} %}
▪ математических выражений, {% for user in userlist %}
▪ списков [‘val1’,‘val2’,‘val3’], {% set proxyname = user[‘lname’] %}
▪ кортежей (‘val1’,‘val2’,‘val3’), {% if proxyname == ‘smith’ %}
▪ словарей {‘key1’: ‘val1’}, {% set proxyname = ‘Кузнецов’ %}
▪ булевых значений true/false.
{% endif %}
<li>{{ user[‘fname’] }} {{ proxyname }}</li>
Сцепление строк - «~» {% endfor %}

Например: Результат:
{{ hostvars[‘service_group_’ ~ var1] }} <li>john doe</li>
<li>john Кузнецов</li>

24
Частая задача #1
Конфигурирование сервиса с учётом
наследования параметров

host2
group1 group2

all host3

host1

1. Сервис умеет в split config (conf.d-style)


2. Используем несколько файлов
3. …
4. PROFIT!!!

25
Решение
Раскладываем значения по файлам:
group_vars/all/service.yml:
service1_group_all: {key1: value1}
group_vars/group1/service.yml:
service1_group_group1: {key1: value2}
group_vars/group2/service.yml:
service1_group_group2: {key1: value3}
host_vars/host3.yml:
service1_host: {key1: value4}
Перебираем все группы хоста (например, для host2):
with_items: "{{ [ group_names ] + [ 'all' ] }}"
Используем где потребуется:
“{{ hostvars[inventory_hostname]['service1_group_' ~ item ][‘key1’] | join(‘,’) }}”

Получим:
“value1,value2,value3”

26
Частая задача #2
Определить большой словарь/dict/hash :-) с повторяющимися частями
---
master_key:
- key_a: val_a
key_b:
- key_c:
- key_d: val_d
- key_e: VAL1
- key_a: val_a
key_b:
- key_c:
- key_d: val_d
- key_e: VAL2
- key_a: val_a
key_b:
- key_c:
- key_d: val_d
- key_e: VAL3

27
Решение
---
inp_list:
- VAL1
- VAL2
- VAL3

master_key: "{%- set temp_hash = [] -%}


{%- for itm in inp_list -%}
{%- set trash = temp_hash.extend([
{ 'key_a': 'value_a',
'key_b':
[{'key_c': [
{'key_d': 'value_d'},
{‘key_e’: itm}
]}]}]) -%}
{%- endfor -%}
{%- set tmp_result = temp_hash | from_yaml -%}
{{ tmp_result }}"

28
Вопросы?

29
Печенко́
Сергей

DevOps/SRE @ Райффайзенбанк

Вам также может понравиться