Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ansible — это вам не bash!

DevOps Moscow
September 26, 2018

Ansible — это вам не bash!

Митап на тему "Infrastructure as Code", 26-09-2018
Сергей Печенко

От взгляда под капот Ansible до написания своего модуля или плагина в несколько простых шагов. Используем Python вместо Jinja.

DevOps Moscow

September 26, 2018
Tweet

More Decks by DevOps Moscow

Other Decks in Education

Transcript

  1. • За клавиатурой 26й год • Ничего не ломаю в

    продакшне с 2013 (нефтегаз, outsource, связь) • Один из админов в @pro_ansible 2 Об авторе
  2. TL;DR • Готовим почву • Об императивном стиле («bashsible») •

    Модули, плагины • Шаблон модуля • Рабочий модуль • Рабочий плагин • Используй Jinja, Люк 3
  3. inventories/ production/ hosts group_vars/ group1.yml group2.yml host_vars/ hostname1.yml hostname2.yml 8X........................8X

    library/ module_utils/ filter_plugins/ roles/ common/ monitoring/ fooapp/ inventories/ environment1/ inventory group_vars/ group1/ service1.yml service2.yml host_vars/ hostname1.yml library/ module_utils/ *_plugins/ roles/ common/ monitoring/ fooapp/ playbooks/ service1.yml Внутреннее устройство проекта Ansible 5
  4. В секции [connection] файла ansible.cfg включаем опцию pipelining=True. Если используем

    переменные окружения - ANSIBLE_PIPELINING=True. Ответ на вопрос “а почему не включено «искаропки»” - потому что конфликтует с опцией команды sudo «RequireTTY», т.к. Python при pipelining «проваливается» в интерактивный режим (см. ссылку) . Включение требует донастройки целевых хостов (как минимум в CentOS). Почему? Потому что технические подробности жизни модуля (Ansible ≥2.1) выглядят вот так: • модуль пакуется в .zip, • Base64-ится, • оборачивается в Python script, • передаётся в целевую среду, • подаётся на stdin интерпретатора Python, • получает из stdin аргументы в виде JSON. Настройки подключения 6
  5. --- - name: My kewl task command: “/usr/bin/cowsay ‘this code

    smells, dude’” - name: Another kewl one-shot task shell: rm -rf / 8 Лёгкий, быстрый, неправильный способ писать плейбуки
  6. Частые «причины» оправдания для bashsible ✓ — У меня нет

    на это времени! ✓ — Это что же, мне теперь всегда следить за конфигами и менять шаблон?... ✓ — Мне это просто сказали поправить, я не эксплуатация ✓ — Там только две строчки заменить надо, остальное по дефолту! ✓ — Мне надо засунуть всё в докер, там конфиг в образе уже работающий! ✓ — Я ненастоящий сварщик девопс, это задание делаю для себя, чтобы научиться! ✓ — Да мне бы только тестовое сделать и сдать! ЛОВЛЯ БАБОЧЕК ТОПОРОМ!!! 9
  7. 1. В своём коде можно (и нужно!) реализовывать: a. идемпотентность;

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

    системе (т.е. удалённо); • но необязательно, см. «connection: local» и много модулей, отвечающих за конфигурирование “умных” железок. В общем случае модулю НУЖЕН Python на целевой системе. Плагин: • выполняется на управляющей системе (т.е. локально); • но необязательно (см. template) В общем случае плагину НЕ НУЖЕН Python на целевой системе. 12
  9. Размещение модуля и его вызов 13 playbook.yml: --- - hosts:

    hostgroup1 become: yes tasks: - name: “New module test” new_module: param1: val1 param2: “{{ var1 }}” param3: ‘fixed text’ Размещение: inventories/ environment1/ inventory group_vars/ group1/ service1.yml service2.yml host_vars/ hostname1.yml library/ module_utils/ *_plugins/ roles/ common/ monitoring/ fooapp/ playbooks/ service1.yml Модуль - сюда! А сюда - все вспомогательные модули, которые встречаются в секции imports вашего модуля.
  10. Минимальный модуль (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 except ImportError, e: print "failed=True msg='failed to import python module: %s'" % e sys.exit(1) 14
  11. Минимальный модуль (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() 15
  12. Реальный модуль (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'),) #.....skipped..... #grafana_[create,delete,export]_dashboard(module, module.params): 16
  13. 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() 17 Реальный модуль (grafana_dashboard.py) (2/2) ⇝
  14. Виды плагинов • action_plugins - для написания своих actions, которые

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

    assert: that: - "my_param <= 100" - "my_param >= 0" msg: "'my_param' must be between 0 and 100" 19
  16. Пример плагина (action_plugins/assert.py) (1/2) # Copyright 2012, Dag Wieers <[email protected]>

    # 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' 20
  17. Пример плагина (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 21
  18. Jinja2: аптечка первой помощи Выражения - то, что непосредственно попадает

    в вывод шаблона. Заключаются в скобки «{{......}}» Доступны константы в виде: • строк, • математических выражений, • списков [‘val1’,‘val2’,‘val3’], • кортежей (‘val1’,‘val2’,‘val3’), • словарей {‘key1’: ‘val1’}, • булевых значений true/false. Сцепление строк - «~» Например: {{ hostvars[‘service_group_’ ~ var1] }} Операторы работают с внутренним контекстом Jinja2 и напрямую в вывод не попадают. {% set userlist = [{‘fname’:‘john’,‘lname’: ‘doe’},{‘fname’:‘john’,‘lname’: ‘smith’} %} {% for user in userlist %} {% set proxyname = user[‘lname’] %} {% if proxyname == ‘smith’ %} {% set proxyname = ‘Кузнецов’ %} {% endif %} <li>{{ user[‘fname’] }} {{ proxyname }}</li> {% endfor %} Результат: <li>john doe</li> <li>john Кузнецов</li> 23
  19. Частая задача #1 1. Сервис умеет в split config (conf.d-style)

    2. PROFIT!!! …. НО БОЛЬ: all group1 group2 host1 host2 host3 Отконфигурировать сервис с учётом наследования параметров 24
  20. Решение Раскладываем значения по файлам: 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’] Получим: value1, value2, value3 25
  21. Частая задача #2 Определить большой 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 26
  22. Решение --- 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 }}" 27
  23. 29