Pull to refresh

Comments 10

Вы честно пытаетесь программировать на Ансибл, даже с java сравниваете. Минимальный набор соглашений по именованиям хорошо иметь, но чем больше вы в него вкладываете усилий, тем больше вы хотите на него полагаться. А этого делать нельзя. Почему?


Потому что:
а) Роли не изолированы друг от друга. vars/main одной роли перебивает defaults/main другой. Вы можете старательно избегать этого используя переменные с префиксами, но это как на ассемблере писать — одна ошибка (или опечатка) и вы уже меняете переменную другой роли самым неожиданным образом. Особенно весело это при выполнени register/set_fact для чужого имени где-то в задворках второстепенной роли
б) Роли не изолированы друг от друга по handler'ам. Это означает, что рано или поздно у вас две роли будут иметь два разных хэндлера с одинаковым названием, и какой-то из них выполнится.
в) Хранение ролей отдельно от плейбук чаще всего смысла не имеет, потому что роль пишется под проект. Чем больше роль пишется как универсальная библиотека, тем больше там желания получить изоляцию, а её в Ansible нет, то есть тем более ужасной становится роль.
г) Не всё можно реализовать на Ансибл и сохранить при этом чистоту кода. Deal with this.


Мы только что закончили рефакторинг куска кода, в котором использовалось перекладывание переменных (связующие переменные), вместо этого переменные используются ровно там, где нужны и не больше. Код стал раз в пять понятнее и проще.


Ансибл — не для программирования.

Написание кода имеет схожие проблемы независимо от языка или инструмента. Если вы начинаете дублировать самого себя (а написание ролей под каждый проект, приводит к дублированию одинаковых мест стека), будет трудно найти ошибку и трудно внести изменения — поскольку вместо одной роли вам нужно перелопатить k (количество проектов в компании) ролей. Также нарушаются принципы унификации и стандартизации.
Поэтому, так важно все же добиться изоляции, чтобы зона влияния роли не выходила за свои рамки, из-за пересечения имен переменных. Ошибки же ловятся и исправляются на тех же этапах, что и в программировании: тестирование и проверка pr. А те, что вышли в свет, оперативно исправляются поскольку ошибка локализована в коде.

Если ansible не может сделать что-то красиво из коробки, нужно делать в виде python модуля с теми же принципами заложенными ansible.
а написание ролей под каждый проект, приводит к дублированию одинаковых мест стека

А нету двух абсолютно одинаковых проектов и требования в них постоянно меняются, поэтому универсальными ролями усеяна дорога в ад, над которым висит неоновая табличка с надписью Ansible Galaxy.


В 99% случаев проще писать роли под каждый проект — пусть их будет 100500, НО при этом каждая из них должна быть максимально простой, настолько, чтобы за пару минут можно было с нуля в неё врубиться. Условно, это 1-2 страницы тупого стандартного ямла с минимумом переменных и максимумом хардкода.


Вот здесь, а не в универсальности ролей, должна быть унификация и стандартизация. Ансибл — не для программирования.

А нету двух абсолютно одинаковых проектов и требования в них постоянно меняются

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


Допустим на проектах используется haproxy, на одном как балансировщик, на другом как реверс-прокси. Пишется роль покрывающая все эти случаи и используется одна, на всех проектах.


Конфигурация в данном случае выглядит достаточно просто:


- hosts: proxy
  roles:
    - haproxy
  vars:
    haproxy:
      defaults:
        log: 'global'
        mode: 'http'
        option:
          - 'httplog'
          - 'dontlognull'

      servers:
        frontend:
          proxy:
            bind: '0.0.0.0:80'
            default_backend: 'service'
            mode: 'http'
        backend:
          service:
            mode: 'http'
            option:
              - 'forwardfor'
              - 'httpchk HEAD / HTTP/1.1\r\nHost:localhost'
            servers:
              local_service:
                url: '127.0.0.1:8080'

- hosts: balance
  roles:
    - haproxy
  vars:
    haproxy:
      servers:
        frontend:
          proxy:
            bind: '0.0.0.0:80'
            default_backend: 'service'
            mode: 'http'
        backend:
          service:
            mode: 'http'
            balance: 'leastconn'
            timeout:
              queue: '3s'
              connect: '3s'
              server: '5s'
            servers:
              global:
                - 'maxconn 500'
                - 'check'
                - 'fall 1'
                - 'rise 3'
                - 'inter 5s'
              service1:
                url: 'service1.fqdn.example:80'
              service2:
                url: 'service2.fqdn.example:80'
                parameters:
                  - 'maxconn 200'
                  - 'check'
                  - 'fall 2'
                  - 'rise 4'
                  - 'inter 10s'

Формируется данный конфиг в процессе шаблонизации jinja:


{% for server_type,configuration in haproxy_install.servers.items() %}
  {% for server,parameters in configuration.items() %}

{{ server_type }} {{ server }}
    {% for parameter,properties in parameters.items() %}
      {% if parameter == 'servers' %}
        {% for instance,params in properties.items() %}
          {% if instance is not in [ 'global' ] %}
  server {{ instance }} {{ params.url }} {{ (params.parameters|default(properties.global|default([])))|join(' ') }}
          {% endif %}
        {% endfor %}
      {% else %}
        {% if properties is mapping %}
          {% for key,value in properties.items() %}
  {{ parameter }} {{  key }} {{ value }}
          {% endfor %}
        {% elif ((properties is not mapping) and (properties is not string) and (properties is iterable)) %}
          {% for value in properties %}
  {{ parameter }} {{ value }}
          {% endfor %}
        {% else %}
  {{ parameter }} {{ properties }}
        {% endif %}
      {% endif %}
    {% endfor %}
  {% endfor %}
{% endfor %}

Естественно, что в роли есть еще дефолты других параметров, например параметры запуска демона для юнита, логирования, пользователи, cipher-сьюты сервера для SSL и так далее, и тому подобное. Все они записываются в дефолты и, при желании переписываются в плейбуке. Ansible не самый удобный CM инструмент для подобных вещей, как например SaltStack или тот же Puppet, однако, при желании, можно и его заставить делать то, что требуется:


Старайтесь не использовать словари для переменных. Ansible не позволяет удобно переопределять отдельные значения в словаре.

nspk — Списки нет, а словари запросто.


haproxy_default:
  daemon:
    options: []
  dirs:
    conf: '/etc/haproxy'
    defaults: "/etc/{{ 'default' if ansible_os_family|lower == 'debian' else 'sysconfig' }}"
    services: '/lib/systemd/system'
    run: '/run/haproxy'
    home: '/var/lib/haproxy'
  shell: "{{ '/usr' if ansible_os_family|lower == 'debian' else '' }}/sbin/nologin"
  global:
    log:
      - '/dev/log local0'
    user: 'haproxy'
    group: 'haproxy'
    ssl:
      ciphers:
        - 'ECDHE-ECDSA-AES128-GCM-SHA256'
        - 'ECDHE-RSA-AES128-GCM-SHA256'
        - 'ECDHE-ECDSA-AES256-GCM-SHA384'
        - 'ECDHE-RSA-AES256-GCM-SHA384'
        - 'ECDHE-ECDSA-CHACHA20-POLY1305'
        - 'ECDHE-RSA-CHACHA20-POLY1305'
        - 'DHE-RSA-AES128-GCM-SHA256'
        - 'DHE-RSA-AES256-GCM-SHA384'
      options:
        - 'no-sslv3'
        - 'no-tlsv10'
        - 'no-tlsv11'
        - 'no-tls-tickets'

haproxy_install: "{{ haproxy_default|combine(haproxy|default({}), recursive=True) }}"

Далее в роли используем мерженные значения из "{{ haproxy_install }}"


Собственно, пример выше, как раз таки показывает как использовать именно унифицированные роли и отказаться от копи-паста и тонн мусорных репозиториев.


Ансибл — не для программирования.

amarao — Нет конечно, но вот шаблонизации через jinja вполне себе.

На нынешней работе один бывший коллега написал универсальную роль для nginx в подобном стиле — нехилый такой Jinja-темплейт и весь конфиг… всего в одной переменной. Но зато какой переменной — понять потом, куда что нужно добавить, чтобы в окончательном конфиге появилось вот то-то и то-то, было очень непросто.


После того, как избавились от этой роли, и стали в каждый проект добавлять практически финальный конфиг nginx с минимумом темплейт-инструкций и максимумом хардкода, дело пошло гораздо быстрее. Даже разработчики начали туда заглядывать и что-то там периодически подтюнивать по своей необходимости. Для нас профит был вполне очевидный.

Наворотить, конечно, можно что угодно — "бумага все терпит".


Главное условие, чтобы интерфейс конфигурирования был удобен и понятен для конечного пользователя, равно как присутствовали примеры и хоть минимальная документация в ридми в репе, тогда проблем не возникнет.
Другое дело, если написанная конфигурация не поддерживает что-то, или просто не известно, как это конфигурировать, ввиду отсутствия доков, и для элементарного использования приходится лезть в кишки, то естественно это работать не будет.


Была у нас конфигурация iptables и, очевидно, использовалась повсеместно — сотни серверов, физических и виртуальных("облака" отнесем виртуальным), разные заказчики и конечно разные среды, так вот за 5 лет эта конфигурация прошла лишь 1 мажорную и 2 минорных версии — в первом случае совместимость конфигурации была частично потеряна, ввиду типа передачи параметров, в двух других случаях, естественно нет, хоть и добавлялась поддержка всех таблиц и цепочек, управление ipset.


P.S. Подобный подход в написании конфигураций успешно применяю более 5 лет, работали с ними мои сотрудники разных уровней компетенции и проблем не возникало, кроме того поставленный процесс CI, содержащий тестирование позволял легко проверить конечный результат в считаные минуты, что делало внесение тех или иных изменений простым и предсказуемым.
P.P.S Сталкивался в работе и с подходом одна конфигурация — один енв, максимум хардкода, копипаст между енвами, неправильный уровень абстракции конфигурационной единицы. Одна закравшаяся ошибка разлеталась на кучи енвов благодаря копипасту, управление становилось настолько проблематичным, что в подавляющем большинстве, внесение изменений осуществлялось вручную, а затем подгонялось в конфигурации под сделанное вручную, в лучшем случае — в худшем оставлялось как есть.
Десятки часов работы, зато все при деле=)

Именно. Хорошо написанный Ансибл не должен привлекать внимания а гордость им должна вызывать удивление "и чем тут гордиться-то? Всё тривиально".


Буквально недавно была история на MR, когда человек написал потрясающе изящную конструкцию, в которой использовались set_fact с делегацией по списку и делегацией фактов.


После 15-минутного обсуждения, вся эта изящная конструкция схлопнулась на перенос переменной в инвентори из одного места в другое и тривиальную роль с loop'ом по собственной переменной.


Было "wow", стало настолько скучно, что ревьюить нечего.


И вот это — хороший Ансибл.

Смотрите, любой язык программирования, за вычетом первых экспериментов и basic'а уровня gwbasic, предоставляет критически важную функцию для написания проекта. Эта функция — изоляция.


Вы пишите модуль (функцию) и никого, кроме ревьюера, не волнует, как называются ваши локальные переменные. Даже если у вас 10 функций в которых есть одна и та же переменая (server_ip), то они друг другу не мешают.


В Ансибле — не так. В ансибле у вас они не просто друг другу мешают, они ещё и создают удивительный мир оверлейных переменных, когда одна роль задаёт server_ip в дефолтах, вторая в vars, третья в set_fact, и догадаться, чья тут переменная сейчас главная — невозможно.


Вы можете сколько угодно бороться за "зону влияния", но вы не можете поменять один простой факт: любая операция с перменной в ансибле даёт side effect. Вы не можете писать чистые функции, вы не можете использовать изоляцию.


И даже если вы свою бизнес-логику вынесете в плагин (не модуль! Модуль вам не поможет), то вы заткнёте один конкретный кейс.


Человечество прошло большой путь от машинного кода для Эниака к языкам с изоляцией и типизацией. Изоляция стала настолько очевидной, что её даже перестали писать в фичах новых языков. Но тут появился Ансибл, который не язык программирования, при применении которого в качестве языка программирования оказывается, что он не предоставляет изоляции. Всё. Добро пожаловать в ассемблер для x86… Хотя нет, там был стек и поддержка call/ret. Когда она появилась первый раз? Вот -1 от этого момента.

Да в Ansible один глобальный namespace в рамках одного хоста. Но ничего не мешает вам разделять одно пространство на множетсво — префиксами. Первый префикс — это имя вашего namespace. Удобнее всего, чтобы это было имя роли. Лично я даже создаю два namespace в одой роли: rolename_vars и _rolename_vars по аналогии с приватными полями в питоне. У меня не будет 10 переменных server_ip, потому что сначала идет название namespace. И в любом языке программирования это работает в коде также ( класс.поле = роль_переменная ). За исключением того, что в них есть локальные переменные, а в Ansible их нет. Кроме программирования эта проблема встречается везде, где есть быстрорастущее пространство имен, и везде оно решается префиксом. ( Например адреса домов: улицы везде одинаковые, но префиксы городов решают ).
В итоге правила просты ( описаны в статье ):
— Не создавайте переменных «в глобальном» namespace.
— Используйте переменные только из своего namespace или dependency.

На самом деле в статье больше акцент на разбор мест где переменные в принципе могут определяться и храниться. И поскольку их много, каждое из них можно превратить в помойку переменных со всего деплоя довольно быстро, даже если сами роли просты.

В Ансибле не просто глобальный неймспейс с глобальными переменными. У Ансибла несколько слоёв глобальных переменных, с разными lifetimes, которые перекрывают друг друга в довольно извращённом порядке.


Вот вы пишите, "не создавайте переменные в глобальном namespace". Чтобы так сделать, нужно:


  1. Всегда использовать loop: index_name (что вызывает wtf в простых случаях, когда, казалось бы, item и вперёд).
  2. Никогда не дёргать setup (потому что setup меняет переменные в глобальном namespace).
  3. Вы себе закрываете любые шансы использовать роли с galaxy, потому что ни вашим принципам не следуют.
  4. Код ролей становится невозможно читать
    - name: common_monitoring | configure dashboard provisioner
    become: true
    copy:
    content: '{{ common_monitoring_node_exporter_dashboard_provider|to_nice_yaml }}'
    dest: '{{ common_monitoring_grafana_outside_config_dir }}/provisioning/dashboards/node_exporter.yaml'
    owner: '{{ common_monitoring_grafana_uid|string }}'
    group: '{{ common_monitoring_grafana_gid|string }}'
    vars:
    common_monitoring_node_exporter_dashboard_provider:
      apiVersion: 1
      providers:
        - name: 'node-exporter dashboard'
          orgId: 1
          folder: 'node-exporter'
          folderUid: '5703111a-97a3-443d-9086-82312a2763a7'
          type: file
          disableDeletion: true
          options:
            path: '{{ common_monitoring_grafana_inside_data_dir }}/dashboards/node_exporter.json'
    notify: restart grafana

Хэндлеры у вас всё равно при этом имеют общий namespace.

Sign up to leave a comment.