2018-02-18

О JSON-RPC

Уже на втором настоящем проекте я внедряю JSON-RPC. Пока никто страшно не жалуется :)
A Logo
JSON — это, как всем известно, JavaScript Object Notation. Я не люблю JSON. Я помню ещё те времена, когда вместо JSON балом правил XML. В XML было всё: и схемы, и трансформации, и многочисленные форматы, протоколы и стандарты, на нём основанные. Хотя бы Jabber/XMPP вспомните.
JSON сейчас полностью заменил или собирается заменить XML. Хотя лучше он лишь по одному параметру: объему сериализованных данных. Ну ладно, ещё нет проблемы «тег или атрибут».
И JSON, и XML годны лишь для автоматической сериализации структурированных данных. Изначальные человекочитаемость и человекописабельность, заложенные в обоих, себя не оправдали. В XML хоть комментарии есть. А вот делать конфиги из JSON, прошу вас, не надо.
И JSON, и XML уязвимы к атакам на парсеры, ибо дозволяют неограниченный уровень вложенности. Иронично, что сам JavaScript парсит JSON документы абсолютно так же, как это делают другие языки. Ибо через eval() — ну совсем уж небезопасно.
В XML всё было текстом. И были существенные проблемы представления бинарных данных. В JSON, вслед за JavaScript, пытаются ввести какие-то типы данных. Но они такие же убогие, как в самом JavaScript. Нет целых чисел. Нет правильных дат. Тут BSON и другие бинарные форматы — вне конкуренции.
XML vs JSON
Ну ладно, есть хипстерский JSON. Значит, должен быть JSON-RPC. RPC — Remote Procedure Call. Как подсказывает Википедия, к RPC относятся такие монстры как CORBA, DCOM. И в Java есть RMI. И NFS внутрях работает через RPC. И на XML у нас имеются широко используемый SOAP и никому неизвестный XML-RPC.
Изначально это наше объектно ориентированное программирование подразумевало пересылку сообщений между объектами. Как-то быстро сложилось, что эти сообщения — это какая-то просьба сделать что-то вот с этими вот параметрами и вернуть результат. То есть вызов метода у объекта. Самые популярные реализации ООП решили, что вызов метода объекта есть то же самое, что вызов функции локального процесса. Все вот эти стеки, переходы, возвраты. Это действительно эффективно. Но также сообразили, что имеет смысл иметь один объект где-то там, и слать вызовы удалённо, по сети, сериализуя и десериализуя параметры и результаты. Это и есть RPC. Ну или микросервисы, если хотите.
Сейчас в этих наших микросервисах, по крайней мере, в вебе, модно использовать REST. Странно. REST гвоздями прибит к HTTP. А HTTP — далеко не самый эффективный транспортный протокол. REST — он про данные. Ну и что, что девяносто процентов всех обращений к серверу — это получение или запись данных. А что делать для остальных десяти процентов? Вводить фиктивные сущности и очереди-коллекции этих сущностей только для того, чтобы POST в эту коллекцию запускал какую-то процедуру?
Есть ещё системы обмена сообщениями. Но в этих самых сообщениях всё равно нужно указать получателя, и что мы от него хотим. RPC — это ведь и есть обмен сообщениями. Где «получатель» — это объект или сервис, а «что мы хотим» — это имя метода. Разница только в том, что в RPC обычно подразумевается синхронный обмен, и в ответ на вызов метода ожидается результат здесь и сейчас. А при «обычном» обмене сообщениями подразумевается асинхронный обмен, ответ либо вообще не требуется, либо придёт когда-нибудь потом, может быть, даже по другому каналу связи. Но синхронность или асинхронность — это скорее особенности организации клиента и сервера, а также работы транспортного протокола, чем принцип собственно RPC.
RPC,Messaging,REST
Вот и JSON-RPC. Это лишь «спецификация» на одной странице, как представлять запросы и ответы в виде JSON. Этой спецификации абсолютно всё равно, каким образом эти запросы и ответы пересылаются. Можно через HTTP, тогда этот JSON просто POSTится на сервер, и тогда имеет смысл замапить варианты ответов на HTTP коды. Можно через голый TCP. Можно через ZeroMQ, мы и так делали.
Но ведь через HTTP, TCP и ZeroMQ можно слать любые JSON сообщения. Зачем использовать JSON-RPC? Потому что вам в любом случае придётся выработать какое-то соглашение. Что кодировать в JSON? Что угодно? А как кодировать ответ? Всё равно ведь придёте к чему-нибудь типа { "success": true }. Вот JSON-RPC и есть такое простенькое соглашение, которое уже есть. И ничуть не хуже любых других соглашений, которые вы можете выдумать.
JSON-RPC не определяет адресацию. Адресация, то есть поиск того самого объекта, на котором будем делать вызовы, это забота транспортного протокола. В HTTP это будет URL эндпоинта, куда POSTтить. В TCP это будет просто хост и порт, куда слать. В ZeroMQ это будет identity получателя или вообще та схема адресации, которую вы выдумаете.
JSON-RPC определяет синтаксис запроса.
{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": { "subtrahend": 23, "minuend": 42 },
  "id": 3
}
Есть обозначение версии протокола ;) Есть имя метода. Есть параметры. Тут может быть либо JSON объект в случае именованных параметров, либо JSON массив в случае позиционных параметров. Но не может быть одиночного значения. А ещё params может быть совсем опущен, если это метод без аргументов.
А ещё у нас есть id. Строка или число. По id клиент сможет связать ответ с запросом. Именно наличие id позволяет JSON-RPC без проблем работать асинхронно. Это уже забота транспорта и клиента, когда и как получить ответ. А по id уже можно понять, ответ на что это был. (Совершенно немыслимая забота в мире HTTP и других синхронных протоколов).
Можно послать сообщение и без id. В JSON-RPC это называется «Notification». В этом случае клиента не беспокоит судьба его запроса и ответ ему не интересен. А сервер не должен на нотификации отвечать. Чем не «обычная» отправка сообщения?
Результат выполнения метода кодируется как result.
{
  "jsonrpc": "2.0",
  "result": 19,
  "id": 3
}
Результатом может быть любой валидный JSON, включая строки, числа, объекты, массивы, и null. (Кстати, при отображении в Java-объекты фиг ты различишь, где там null, а где отсутствие данного поля).
Если что-то пошло не так, результата не будет, будет error.
{
  "jsonrpc": "2.0",
  "error": { "code": -32601, "message": "Method not found" },
  "id": "1"
}
В error имеется код. Есть несколько предопределённых кодов для типичных ситуаций (вроде отсутствия метода), но для специфичных ошибок вам придётся придумать свои коды. Имеется сообщение, типа человеку. И можно добавить data с чем угодно, jsonrpc4j туда пихает класс и сообщение из исключения.
Ну вот и вся спецификация. Есть ещё батчи, но там всё очевидно. Просто? Понятно? Имхо, да.
JSON-RPC Format
Мы в спринговых микросервисах используем jsonrpc4j. Кажется, это единственная серьёзная реализация для Java. Там есть свои клиенты и серверы, и какая-то интеграция со Spring. В результате получается весьма прозрачно, как оно и положено в RPC. Есть интерфейс, с объявленными методами, с заданными именами параметров. На серверной стороне есть реализация этого интерфейса, которая и представляет собой этот наш объект, который будем удалённо вызывать. На клиентской стороне создаётся Proxy, который подключается к серверу, и реализует этот самый интерфейс для локальных вызовов. В качестве параметров и результатов можно использовать всё что угодно, пока оно сериализуется Jacksonом. Получается красиво, но синхронно.
Очень хочется этот jsonrpc4j слегка допилить. Убрать задавание path для HTTP сервера в интерфейсе, всё же это личное дело сервера, по какому адресу располагать эндпоинт, а значит, path должен быть задан у реализации, а не у интерфейса. Убрать странные и дурацкие ограничения на этот path. Например, что мешает на один path навешать кучу объектов, пока они не пересекаются по методам? Добавить поддержку автоматического распознавания имён параметров, ибо это должно работать в Java 8 и Kotlin. Но пока некогда... И так неплохо работает.

2018-01-27

Об AWS

Они говорили: «Большие облачные провайдеры — это хорошо». Они говорили: «Там есть отличная поддержка Docker контейнеров». Они говорили: «Контейнеры идеально подходят для микросервисов». Они говорили: «Микросервисы — это лучшая современная архитектура».
AWS Logo
На прошлой неделе меня развлекала Azure. На этой неделе мы переехали на Amazon Web Services aka AWS. В надежде, что в AWS будет меньше нежданчиков. Ну и просто, AWS был привычнее, и заказчику, и мне.
Нежданчики были. Но они имели объяснение, и часто даже описание в документации.
Документация в AWS мне нравится больше. Вроде объем сравним с азуровской. Но технических подробностей больше. Объясняется, что это, зачем это и с чем это взаимодействует. Меньше пошаговых туториалов со скриншотами. Почти нет маркетинговой чепухи.
Консоль управления (не Portal, а Console) — другая. В Азуре всё напихали панельками в одно большущее веб-приложение. В Амазоне слепили из лоскутков чудовище Франкенштейна. Но это хорошо. Каждый лоскуток — сам по себе, прост и понятен. Разные лоскутки — консольки разных сервисов, оформлены немного по-разному, в разных стилях. Где-то подновлено, попросторнее, настройки чётко сгруппированы. Где-то более старенькое, с большущими боковыми меню.
Единственный мозговзрывающий пункт — список всех сервисов. Его сделали, ура, в несколько колонок на весь экран. И только текстом. И с поиском. Не заблудишься. И, ура, отдельно вынесены последние сервисы, которые открывал.
Самой большой проблемой с консолью были права доступа. Мне не дали суперадмина на весь аккаунт. Пришлось постоянно натыкаться на отсутствие прав и запрашивать их отдельно. И в Азуре, и в Амазоне сущностей, на которые можно раздать права, так много, что просто невозможно заранее выдать нужные ограниченные права.
CLI тоже есть. Известный aws. Тоже написан на Python, хошь на втором, хошь на третьем. Под капотом — библиотечка boto.
В отличие от Азуры, где клиент срёт кучей разных файлов в ~/.azure, с aws и boto всё чётко. Есть лишь два файла: ~/.aws/config, где прописывается нужный регион (десятки их у Амазона), и ~/.aws/credentials, где нужно указать ключ и секрет. И эти файлы нужно создать руками. Можно и несколько профилей указать, если одновременно нужно в разных аккаунтах рулить. Ключа и секрета достаточно, чтобы от имени владельца ключа рулить Амазоном. И из Ansible тоже.
Ансибль может AWS. По сравнению с Азурой, модулей больше, они разнообразнее, они старше минимум на пару лет. Но некоторые свежие фичи, которые в AWS завезли в 2017, Ансиблем ещё не поддерживаются.
Console screenshot
Single Page Applications aka SPA, типа на React, можно заливать непосредственно в файлопомойку Амазона под названием S3. Что, кстати, расшифровывается как Simple Storage Service, любят они слово «Simple». И прикрыть это дело CDN под называнием CloudFront. В обоих сервисах магия сводится к настройке, чтобы в случае любой ошибки, в том числе 404 Not Found, возвращать /index.html.
SPA Lifecycle
Докеры. Реестр для образов имеется: Elastic Container Registry aka ECR. Слово «Elastic» в Амазоне любят ещё больше.
В отличие от Азуры, где пароль для docker login выдаётся раз и навсегда, Амазон выдаёт длииииинющий пароль, который действует лишь 12 часов. Мол, раз вы пароль передаёте в docker login, то он остаётся в истории команд, а это нехорошо. Ну и кто из них больше думает о безопасности?
Новый пароль приходится получать командой aws ecr get-login --no-include-email. Или даже так:
$ $(aws ecr get-login --no-include-email)
Потому что выхлопом команды является уже готовый к исполнению docker login с аргументами. Так что бы не выполнить его сразу?
Можно прикрутить ECR Credentials Helper, штуку такую, расширение для Докера, чтобы Докер сам ходил в Амазон за авторизацией. Но что-то у меня оно сразу не заработало.
В отличие от Азуры, Docker Hub и прочих Docker Registry, ECR не создаёт репозиторий для образа автоматически. Нельзя сделать docker push для образа, про который ECR ещё ничего не знает. Нужно явно создавать репозитории, через консоль, через CLI или через Ansible.
ECR, ECS relations
Образы есть. Они вполне доступны из реестра образов. Теперь нужно деплоить контейнеры. Для этого есть Elastic Container Service aka ECS. Эта штука не совместима ни с чем. И работать с ней нужно через консольку, CLI или Ansible.
Для начала нам нужен ECS кластер. В котором будут выполняться задачи (Task), представляющие собой какие-то сервисы (Service), описанные в определении задач (Task Definition).
В Ansible определение задачи выглядит примерно так:
- name: create ECS task definition
  ecs_taskdefinition:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    family: '{{ service_name }}'
    network_mode: host
    containers:
    - name: '{{ service_name }}'
      image: '{{ service_image_name }}'
      memoryReservation: 64
      portMappings:
      - containerPort: '{{ service_port }}'
      logConfiguration:
        logDriver: 'awslogs'
        options:
          awslogs-group: '{{ ecs_cluster_name }}'
          awslogs-region: '{{ aws_region }}'
          awslogs-stream-prefix: '{{ ecs_cluster_name }}'
      environment:
      - name: SERVER_PORT
        value: '{{ service_port }}'
      - name: DATABASE_URL
        value: 'jdbc:postgresql://{{ service_database_host }}/{{ service_database_name }}'
      - name: DATABASE_USER
        value: '{{ service_database_user }}'
      - name: DATABASE_PASSWORD
        value: '{{ service_database_password }}'
  register: task_output
Для консольки или CLI то же самое нужно переписать в JSON. Поэтому, кстати, внутри определения контейнера появился camelCase, не свойственный Ансиблю.
Как видите, тут нужны ключ и секрет для доступа к AWS.
family — это «семейное» имя для определения задачи. Определения версионируются, поэтому актуальное имя определения будет вида «family:revision».
Про network_mode поговорим попозже.
В одной задаче можно запустить сразу до десяти контейнеров. Это будет одна единица масштабирования. Эти контейнеры будут гарантированно запущены на одном Docker хосте. Раз на одном хосте, у них будет прямая сетевая видимость друг друга.
Контейнер, как положено, запускается из определённого образа, image.
Контейнеру обязательно нужно указать требования по памяти. memoryReservation — это мягкий лимит, в мегабайтах.
Как обычно в Docker, можно прокинуть порты, portMappings. Порт внутри, порт снаружи. Впрочем, для network_mode: host порт нужен только один.
logConfiguration — это такая специальная штука, где с помощью драйвера awslogs можно засовывать логи контейнера в CloudWatch — фиговину для мониторинга.
В environment контейнеру передаются переменные окружения, как обычно.
Результат создания описания задачи мы записываем в task_output, потому что он будет нужен для создания сервиса.
Сервис из Ansible создаётся так:
- name: create ECS service
  ecs_service:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    name: '{{ service_name }}'
    cluster: '{{ ecs_cluster_name }}'
    task_definition: '{{ task_output.taskdefinition["family"] }}:{{ task_output.taskdefinition["revision"] }}'
    desired_count: 1
    deployment_configuration:   # allows to shut down and restart the service
      minimum_healthy_percent: 0
      maximum_percent: 100
Снова нужны ключи доступа в Амазон.
name — это имя сервиса. Уникальное в пределах кластера.
cluster — это имя кластера. Он уже должен быть. Так как его обновлять не нужно, проще создать его один раз из консоли. Хотя, конечно, есть и другие варианты.
task_definition — это определение задачи для этого сервиса. Тут нужно указать конкретное определение, включая его фамилию и ревизию. Именно для этого мы записывали результат создания определения в task_output.
desired_count — сколько экземпляров сервиса мы хотим видеть живыми. Получается, что сервис — единица масштабирования. И все контейнеры, объявленные в определении задачи, будут созданы в данном количестве экземпляров.
Но не обязательно. deployment_configuration задаёт требуемые параметры «живости» сервиса. По умолчанию, если мы сменили определение и перезапустили сервис и его контейнеры, ECS сначала создаст вторую копию сервиса, а лишь потом удалит старую копию. Он стремится к zero downtime. Но это создаёт проблемы, если контейнер занимает определённый порт хоста, а хост с Докером у нас только один. Второй контейнер на том же порту не запустишь, поэтому всё ломается. В этом примере разрешается нулевая «живость», и сначала убивается старый контейнер, а потом создаётся новый.
Tasks in ECS
В ECS нужно создать кластер, который будет рулить сервисами, создавать задачи по определениям, в нужном количестве. Но что такое кластер? Тут есть два варианта.
Недавно в AWS появилась штука под названием Fargate. Это магическая фиговина, которая запускает контейнеры незнамо где. Имхо, довольно удобно. И ECS, конечно же, умеет работать через неё. Но Fargate пока доступен только в одном датацентре Амазона, в Северной Вирджинии.
Остальным нужно запускать кластер на старых добрых инстансах EC2, то есть на виртуалках. Всё честно, вам сразу говорят, нужны виртуалки, чтобы запускать контейнеры. На этих инстансах нужно поставить Docker и запустить ECS агента. Но логичнее и проще взять готовый ECS-optimized образ виртуалки, где всё уже стоит. Можно запускать EC2 инстансы не руками, а через Auto Scaling группу. Тогда ECS сам будет заботиться, сколько инстансов нужно, в зависимости от ресурсов, которые будут есть контейнеры.
Вся эта магия делается за минуту через визард создания кластера в консоли. Тем не менее, все кишки и задействованные системы торчат наружу, и их можно потом поднастроить.
Важен один нюанс. EC2 инстансы должны иметь выход в интернеты. Ибо агентам на них нужно общаться собственно с ECS, а он для них находится где-то там.
Кстати, Auto Scaling можно включить и для сервисов в ECS. Тогда количество задач, то есть количество запущенных контейнеров для этого сервиса, тоже будет динамически подстраиваться в зависимости от нагрузки.
ECS Instances
Контейнеры деплоятся. Хорошо. Но у нас микросервисы. К каким-то сервисам нужен доступ снаружи. А какие-то сервисы нужны для внутренних нужд. Нужно, чтобы сервисы знали друг о друге и имели прямой сетевой доступ друг к другу.
Собственно, эти вопросы в рамках одного хоста умеет решать Docker Compose. Контейнеры видят друг друга по имени сервиса, указанного в docker-compose.yml. И вполне имеют сетевой доступ друг к другу, если находятся в одной сети. В рамках нескольких Docker хостов эти вопросы решает Swarm, с помощью своей overlay сети. Kubernetes, полагаю, имеет схожие механизмы.
Что нам предлагает ECS для организации сети между контейнерами? Он нам предлагает то же, что голый Docker Engine двухгодичной давности. Плюс одна амазоноспецифичная плюшка.
network_mode: host. В этом случае никаких дополнительных сетевых прослоек не имеется. Процесс в контейнере слушает порт на хост машине. Соответственно, на одном порту можно запустить лишь один контейнер на хост. Нужно больше? Тогда нужно больше EC2 инстансов cнизу. Просто и эффективно. Но в общем случае контейнер не знает, на каких EC2 инстансах (с какими IP адресами) запущены другие контейнеры.
network_mode: bridge. Обычный сетевой мост Докера. Внутри контейнера процессы могут вешаться на любой порт. Этот порт может быть выставлен наружу, то есть на EC2 хост, под любым другим номером. Контейнеры на одном EC2 инстансе могут ходить по сети друг к другу. Но, во-первых, нет никакой гарантии, что они окажутся на одном инстансе, только если не были определены в одном ECS таске. Во-вторых, они всё равно не знают ни доменных имён, ни IP адресов (внутренних) друг друга. Поддерживаются старые добрые линки. Но это опять-таки работает только в рамках одного EC2 хоста. К тому же линки требуют, чтобы контейнер, на который ссылаются, был уже запущен. А такой жёсткий порядок запуска, мягко говоря, не удобен.
network_mode: awsvpc. Новая штука. Поддерживается в Fargate. Требует дополнительной настройки агента на EC2 инстансах. В этом случае каждый контейнер становится полноценным участником сетевого обмена в инфраструктуре AWS. Каждый контейнер получает свой собственный elastic network interface в рамках VPC. Проблема в том, что количество этих интерфейсов на один EC2 инстанс ограничено, от двух до пятнадцати, в зависимости от размера инстанса. А это значит, что на одной EC2 машинке можно будет запустить лишь от двух до пятнадцати контейнеров ECS.
Bridge network
В любом случае всё остаётся на уровне голого Docker Engine. Механизма контейнерам обнаружить друг друга и ходить друг другу в гости ECS не предоставляет.
В моих микросервисах уже сложился свой механизм service discovery. Я лишь решил слегка допилить его. Хранить реестр сервисов во внешнем Redis, который в AWS представлен в виде ElastiCache. А код для регистрации себя и обнаружения других сервисов встроить в сами сервисы.
Кстати, узнать IP адрес текущего EC2 инстанса (и контейнеров в нём) легко посредством запроса к Instance Metadata. Буквально вот так:
$ curl http://169.254.169.254/latest/meta-data/local-ipv4
Существуют и более амазоноспецифичные способы самообнаружения контейнеров. Можно запрашивать Load Balancer, ибо он может знать обо всех контейнерах, которые он балансит, где они находятся и на каких портах. Можно внедрить в каждый контейнер агента, который будет регистрировать адрес и порты контейнера в амазоновом DNS (который называется Route 53).
Можно рассматривать это как прикладное применение теоремы Гёделя о неполноте. Контейнеры сами по себе не могут решить задачу самообнаружения. Нужна внешняя сущность в виде базы данных (хотя бы DNS сервер), к которой имеют прямой доступ все контейнеры.
Discovery with DNS
Контейнеры есть. Друг к другу ходить могут. Как обеспечить к ним доступ извне? Логичнее всего использовать Elastic Load Balancing aka ELB. Его вариант, разбирающийся в HTTP, под названием Application Load Balancer.
Балансировщик нагрузки принимает входящий HTTP или HTTPS трафик. Можно навешать свой домен. Можно навешать свой сертификат. И по правилам направляет запросы на Target Groups. В качестве правил можно использовать либо пути, либо заголовки запроса.
В целевых группах указываются либо EC2 инстансы, либо IP адреса, в рамках VPC. Но ведь наши EC2 инстансы для наших контейнеров могут создаваться и удаляться, autoscaling ведь. Чтобы решить эту проблему, ECS может управлять балансировщиком, прописывая туда, где сейчас и на каком порту висит соответствующий сервис. Нужно только в настройках сервиса указать, каким балансировщиком и какой Target Group управлять.
Application Load Balancer в AWS — такая же тупая штука, что и в Azure. Url rewriting не умеет. Проверяет работоспособность бэкендов HTTP GET запросами. Так что все прибабахи для сервисов, типа специального контроллера на /health, остаются в силе. А ещё он не умеет CORS.
CORS aka Cross-Origin Resource Sharing — это такая фигня, которую современные браузеры делают, когда API, куда стучится JavaScript, находится не в том же домене, что страница, на которой выполняется JavaScript. Вполне полезная штука в плане безопасности. Браузер делает OPTIONS запрос, спрашивая: «А вот с этого домена можно?» Сервер отвечает в заголовке Access-Control-Allow-Origin: «С этого можно».
Поддержку CORS нужно делать в контейнере. Можно водрузить Nginx. Можно добавить чуток конфигурации в Spring.
Есть в AWS и более мощная штука под названием API Gateway, которая это всё вроде может. Но API Gateway — это скорее про конструирование API-заглушек, например, для мобильных приложений. Чтобы потом заполнять их вызовами кода из Lambda. Это скорее не про микросервисы, а про конструирование API в облаке на лету, API as a service. Боязно и странно.
Load Balancing
Ладно. Есть докеры двухгодичной свежести. А есть что поудобнее? Есть. Elastic Container Service for Kubernetes aka EKS. Похоже, это заразно, играть буквой «C» в аббревиатурах. Container → Kontainer → Kubernetes.
Вот только EKS в Амазоне «is in Preview». А превью в Амазоне — значительно серьёзнее, чем в Азуре. Это значит написать им, объяснить, зачем нам это надо, и ждать две недели, может быть включат.
EKS
С этими облаками и контейнерами всё получается так. Сначала создаются все эти сущности: балансировщики, кластеры, репозитории. Потом собираются образы и выкладываются в реестры. Потом запускаются или перезапускаются контейнеры по этим образам.
Ансибль в этом мире Immutable Infrastructure выглядит чужеродно. Ибо всё равно все его задачи выполняются на localhost с типом подключения local. Всё равно ведь нужно к AWS API подключаться.
Чувствую, что, когда в следующий раз пойду за облаками, я возьму Terraform. Ты просто описываешь, что, где, с какими свойствами, с чем связанное, в каком количестве тебе развернуть. И оно разворачивает. У AWS есть своя такая штука, CloudFormation называется. Но Terraform более переносим, он умеет не только AWS.
Terraform vs Ansible

2018-01-21

Об Azure

Microsoft Azure. Это такое облако с виртуалочками и всем таким. Как Amazon Web Services (aka AWS), только Microsoft. Пришлось на этой неделе поиметь с ним дело.
Заглянем в словарь. Оказывается, «azure» — это «лазурь», цвет такой, оттенок голубого. И ещё сорт ячменя.
Azure Logo
Начинаются все облака, конечно, с IaaS, то есть с виртуалок. В Azure вроде всё хорошо. Виртуалки есть. Они работают. Их можно засунуть в виртуальную частную сеть. Им можно индивидуально выдать публичные IP адреса или навесить доменное имя.
Когда я попытался самолично создать виртуалку, я запутался в чудовищном количестве доступных конфигураций. Ужос случился потом, когда оказалось, что самая дешевая конфигурация с достаточным количеством ЦПУ и памяти имеет чудовищно медленный диск. Настолько медленный, что банальный apt install htop выполняется минуты четыре, тормозя именно на построении индекса доступных пакетов.
Ассортимент виртуалок явно требует отдельного вдумчивого изучения. С выписыванием всех характеристик и цен в какую-нибудь отдельную табличку. Калькулятор цен есть, и даже какую-то правду показывает. Но он не способствует сравнению и выбору подходящей конфигурации.
Для управления облачным хозяйством есть Портал. Выполненный в стиле Microsoft, или Visual Studio, или Windows 10. Как хотите. Суть в том, что всё квадратное, куча выползающих и наползающих со всех сторон панелек. Каждая панелька, когда появляется, секунд 5-10 крутит спинером, прежде чем отобразить то, что нужно. URLы нечитабельные и часто некопипастебельные.
Сущностей тонны. Каждую нужно нежно настраивать. Связи между сущностями никак не очевидны. Свойства сущностей случайным образом сгруппированы. Бывает, о ужас, идентификаторы и ключи одной сущности нужно копипастить в поля другой сущности. Руками. Копипастить.
Как интерфейс, призванный помочь в этом разобраться и грамотно настроить, портал Азуры — полное говно.
Есть ещё командная строка. Утилитка az. Написана на Python 3. Теоретически, она может всё, что может Портал. Но она может даже больше. Есть некоторые команды, которые, если повторить в Портале, потребуют пятнадцати минут кликов и копипастов, чтобы создать с десяток связанных сущностей. Узнать, какие команды, где и зачем, можно из туториалов. Коих, слава богу, довольно много.
Для тех, у кого есть PowerShell, есть модули для PowerShell. А PowerShell вроде как можно завезти даже в Linux.
Ansible тоже может Azure. Посредством питонячьего модуля. Но вот лезть в Азуру из Ансибля как-то не пришлось. Ибо у Docker Registry и Kubernetes есть свои более специфичные API, с которыми Ансибль вполне прекрасно работает. Модули же для работы с Azure меня напрягли тем, что они пытаются создать всю тонну сущностей, которая ведёт, допустим, к деплою файлика в Blob Storage. Это требует и дополнительных прав на создание этих сущностей, и нет гарантии, что дефолтные параметры этих сущностей будут теми, какие надо. Особенно, когда ты вообще не знаешь, какие там сущности нужны, и какие параметры стоит выставить. Слишком много магии в одной команде.
Azure Portal
Первая проблема с Azure. PostgreSQL. C MS SQL всё должно быть прекрасно. А вот managed MySQL и PostgreSQL живут в статусе «preview». Типа, всё может поломаться. И ломается.
Инстансы PostgreSQL живут сами по себе. Со своим публичным, на все интернеты, IP адресом и доменным именем. Их нельзя засунуть в виртуальную сеть, где живут ваши виртуалки. Интересно, сказывается ли это на стоимости трафика? Спасибо, есть файервол.
У нас так случилось, что PostgreSQL был доступен откуда угодно, но только не из виртуалок в том же датацентре. Оказалось, что виртуальная сеть этих виртуалок была сконфигурена на доступ к MS SQL, и PostgreSQL при этой конфигурации недоступен. Для всей виртуальной сети. Странно. Самостоятельно разрулить проблему мы бы не догадались, помогла поддержка.
PostgreSQL access
Немного забегая вперёд, подняли один сервис на обычной виртуальной машинке. Как дать доступ к сервису снаружи? Через HTTPS конечно же. Можно выдать виртуальной машинке публичный IP, и разбираться с сертификатами самостоятельно. Но ведь машинка может переехать. Или понадобится больше машинок, микросервисы ведь. По-хорошему, нужен какой-то reverse proxy с правильным и зафиксированным доменным именем, который бы прикрывал собой любую мешанину микросервисов, и занимался бы HTTPS.
В Азуре для этого вроде как имеется Application Gateway. Можно навесить на него свой домен, и скормить сертификат. Кстати, сертификаты Azure не выдаёт, и получить Let's Encrypt сертификат на домены служб Azure (тысячи их) тоже не выйдет, а не все эти домены свои сертификаты уже имеют.
Можно навесить правила, на какой сервис позади отправлять вот этот вот запрос. Можно делать это по пути в запросе. Например, пусть все запросы на «/service1/*» идут на наш первый сервис. При этом было бы логично, чтобы эта начальная часть пути, по которой мы выбираем бэкенд, удалялась из URL, чтобы сам сервис не зависел от того, по какому пути его вывешивают наружу. Но нет, даже такого простого и логичного URL rewriting Application Gateway не умеет. В результате, если у нас там Java, приходится накручивать правильный context path. И ведь не то чтобы он совсем не умел переписывать запросы, «X-Forwarded-For» ведь проставляет.
А ещё этот Application Gateway очень своеобразно проверяет жизнеспособность бэкенда. Существует лишь один единственный способ для этого. Gateway делает GET запрос на бэкенд. Хорошо, что path в этом запросе можно задать. Беда в том, что наш грешный сервис был сделан только для того, чтобы принимать POSTы. Пришлось в него добавить ещё один контроллер, чтобы отвечал на «GET /service1/health». Кажется, я избалован HAProxy, с его тучей способов проверки доступности бэкендов.
Application Gateway
Но тем не менее, основная задача немного другая. Хотим задеплоить фронтенд на React. И кучку микросервисов в виде бэкенда для него. Хотим делать это serverless, то есть без виртуальных машин, а контейнерами Docker.
Фронтенд. Single page application (aka SPA) на React — это же статичные файлы: HTML, JS, CSS, немного картинок. Их же можно засунуть в простейшее файлохранилище с доступом по HTTP или даже завернуть через CDN. Так? Нет, но об этом позже.
В Азуре есть CDN. Он может принимать HTTP или HTTPS запросы, запрашивать ответы в статических хранилищах, веб сервисах или вообще куда угодно, и распределённо кэшировать ответы в нужном количестве датацентров. Можно навесить свой домен и SSL сертификат. Нормальный CDN.
Для хранения файликов за CDN наиболее подходит Blob Storage. Аналог амазоновского S3. Всё просто. Заводите себе storage, получаете домен для него. Заводите там то, что называется container, это будет путь в URL. Заливаете файлы-блобы. Имена блобов могут содержать слэш «/», что эквивалентно созданию подкаталогов. Ну и надо правильно указать mime тип, что может быть проблемой, как минимум, если заливать файлы из Ansible.
Но погодите. Имя контейнера становится частью пути. А если мы хотим наши файлики положить в корень? Для этого есть секретный контейнер с именем «$root». Окей. Но, внезапно, в контейнере «$root» нельзя создавать блобы со «/» в имени, то есть нельзя создавать подкаталоги. Причём официально про это сказано ничего. Ошибка, которая при этом возникает, совершенно невнятная, и похожа на ошибку в коде клиента, заливающего блобы. Может, кстати, так оно и есть. Ребята на Stack Overflow, обнаружившие такую особенность контейнера «$root» эмпирически, не углублялись в исследования причин.
Ладно. Залили index.html в «$root». Залили всякие CSS и JS в «static». Получится запустить SPA? Нет. Потому что «http://какой-то-домен.blob.core.windows.net/» не открывает index.html. Нет такого понятия, как directory index, ни в Blob Storage, ни в CDN. А в S3 — есть.
Но directory index тоже недостаточно. В SPA по-хорошему нужно, чтобы все запросы, которые не идут на CSS и JS, выдавали index.html. Чтобы можно было бы спокойно манипулировать адресной строкой, и этот адрес всегда бы приводил на index.html.
Поэтому все пути по хостингу реактовых приложений на Azure ведут на Web Apps. А Web Apps — это те же контейнеры, но с IIS, PHP или Node.js внутри. Ну и зачем мне контейнер с Node, если достаточно простого Nginx? Круг замкнулся. Нужны докеровые контейнеры.
Blob Storage
Шаг первый прост. Нужно собрать образы и положить их в Docker Registry. В Азуре есть такой, называется Container Registry. Тут всё нормально и без сюрпризов. Создаём реестр. Получаем домен типа «чего-то-там.azurecr.io», и логин-пароль к нему. Делаем docker login чего-то-там.azurecr.io. Навешиваем на наши локальные образы теги вида чего-то-там.azurecr.io/имя-образа. Делаем docker push. Всё прекрасно, и можно не приплетать инструменты Azure, а пользоваться стандартными докеровыми командами. В том числе и из Ansible.
А теперь эти образы нужно развернуть. И тут, внезапно, есть три способа.
Container Instances. Фича из категории «preview». Странный, ни с чем не совместимый способ. Просто указываем образ и запускаем контейнер с публичным IP адресом. Учитывая, что у меня микросервисы, совсем не хотелось их все выставлять голой попой в интернеты. Наоборот, хотелось их все засунуть в приватную локалочку, и уже оттуда открывать только то, что нужно. Ну как это обычно делается с Docker Compose. Поэтому Container Instances были сразу же отброшены за неспортивность.
Azure Container Service (ACS). Не путать с AKS. Штука, помеченная в документации как «old version». Создаёт нормальный кластер управления контейнерами. В теории, либо DC/OS, либо Kubernetes, либо Swarm. На практике, в документации найти можно только Kubernetes. Только с этим сервисом контейнеров напрямую может работать Ansible.
Azure Container Service (AKS). Не путать с ACS. Да, блин, два сервиса с совершенно одинаковыми названиями. Этот помечен как «preview». Тут только Kubernetes. На самом деле, Kubernetes API, которое тут есть, вполне достаточно, чтобы деплоить контейнеры.
Оба, и ACS, и AKS, как-то странно вписываются в инфраструктуру Azure. Они оба хотят для своего исключительного использования отдельную Resource Group. Resource Group — это и namespace, и точки приложения прав доступа, полагаю, и биллинговая единица в Азуре. Все ресурсы принадлежат определённой группе. В рамках проекта у меня не было прав на создание ресурсных групп. Это сильно замедлило эксперименты с развёртыванием контейнеров.
Обоим сервисам при создании нужно скопипастить credentials штуки, которую в документации называют Service Principal. По сути, это OAuth параметры доступа к аккаунту для других приложений. Понятно, когда таким другим приложением является Ansible, запускаемый у меня на ноутбуке. Но непонятно, почему таким приложением является другая половинка Azure облака в том же датацентре.
Для обоих сервисов при создании нужно указать количество узлов кластера и их размер. Размер, в смысле тип виртуалки, на которых это будет запущено.
Но погодите. Это же managed сервис. Какие виртуалки? И за них полностью платить? Ну и чем это принципиально отличается от того, что я на этих виртуалках самостоятельно запущу тот же Kubernetes?
Получилось создать AKS. Вот только подключение к Kubernetes API отваливалось на стадии TLS handshake. Рабочая версия, почему такое говно, — видимо, я снова выбрал слишком дешевую и медленную виртуалку, которая не может переварить handshake за две минуты дефолтного TCP таймаута.
Kubernetes in Azure
Ну и стоит ли продолжать с Azure? Или на всё плюнуть и уехать на AWS? Пока ещё ничего толком не развёрнуто.
На AWS бывают откровенно сырые фичи? Как там с Докером?

2018-01-07

Об Ansible

Начнём новый год с буквы «A». Ansible.
Кстати, Анзи́бль — это такая фантастическая фиговина для мгновенной связи на межгалактических расстояниях.
А Ansible — это программка для удалённого запуска команд и совершения прочих нужных действий на куче серверов. Мы её используем для настройки серверов и деплоя наших сервисов уже на нескольких проектах.
Ансибл — чертовски гибкая штука. Настолько гибкая, что без определённых соглашений все эти таски и плейбуки легко превращаются в нечитабельную немодифицируемую глючную лапшу. Чего только стоит, что переменные в Ансибле можно определять индивидуально для каждого хоста, для группы хостов, умолчательные значения для роли, просто переменные для роли, переопределять в плейбуке, из командной строки, конкретно для данной задачи. А ещё переменные могут создаваться в результате выполнения задач, и использоваться в последующих задачах. А ещё есть факты, то есть переменные, автоматически собранные при инспектировании хостов, типа объема памяти, размера дисков и набора сетевых интерфейсов. Ужос.
Эмпирическим путём у нас сложились правила использования Ансибла. В основном вдохновлённые и следующие статье Миши Бланка «Laying out roles, inventories and playbooks».
Ansible logo
Все ансибловые файлы у нас валяются в подкаталоге ansible самого проекта. Вообще, идеология Ансибла подразумевает, что должны быть универсальные переиспользуемые роли, выкладываемые в Ansible Galaxy, единый конфиг в /etc, единый набор Inventory на все окружения, с которым работает данный юзер, и лишь плейбуки, специфичные для данного проекта.
Но на практике с универсальностью ролей всё плохо. Во-первых, страшно запускать фиг знает кем написанную роль от рута на десятках серверов. Во-вторых, написать действительно универсальную роль весьма сложно, поэтому роли из Galaxy всё равно приходится подпиливать по свои нужды. Проще написать неуниверсальную роль для данного проекта. В-третьих, роли очень быстро устаревают, ибо версии софта, который они устанавливают, стремительно меняются. А мейнтейнить проще простые специфичные роли.
Вот поэтому у нас не сложился общий репозиторий ролей. Некоторые роли просто копируются из другого проекта. А остальные 90% пишутся под этот проект. Всё лежит под системой контроля версий проекта, и всё хорошо.
Начинается всё с файла ansible/ansible.cfg:
[defaults]
inventory = inventories/development
inventory_ignore_extensions = ~, .retry, .pyc, .pyo, files, templates
roles_path = roles
pipelining = True
Ansible variables
Далее Inventory. Инвентарь. Это ваши хосты. По группам. Инвентарей будет много. Потому что окружений будет много: ну это всё тестовое, продакшен и т.п. На каждое окружение — свой Inventory.
В hostfile стоит прописать значащие имена хостов. Лучше доменные имена, но если доменных имён нет, стоит что-нибудь внятное вместо них придумать, а правильный IP указать как ansible_host. Ну и юзера, и ssh порт, и прочие параметры подключения для каждого хоста указать.
[etl-server]
etl1 ansible_host=172.18.40.37 ansible_user=ubuntu ansible_become=yes
В группу должны входить абсолютно одинаковые хосты. Ну которые нужно одинаково настроить. Потому что переменные удобнее и правильнее назначать на группы, и в плейбуках правильнее вызвать роли на всю группу. Разве что первый (нулевой) хост в группе может быть особенным (для хостов из других групп), на него можно ссылаться как {{ groups['etl-server'][0] }}.
Кроме перечисления собственно хостов и групп в Inventory можно поместить шаблоны и файлы. Вообще-то, это не Ansible-way. В Ансибле подразумевается, что шаблон должен быть один, где-нибудь в роли. А все нюансы и хитрости под конкретное окружение должны задаваться переменными, а также условиями и циклами в шаблоне. Это ж Jinja2, там всё можно.
Вот только сопровождать сложный универсальный шаблон, как правило, сложно. Ну и юзерам ещё и шаблонизацию Ансибла объяснять? Ты тут только-только устаканил набор всех возможных переменных в application.yml, более-менее описал их все в Вики. А теперь ещё и шаблон родить, и переменные Ансибла придумать, и снова это всё задокументировать?
В общем, имеет смысл файлы и шаблоны складывать в Inventory тоже. Если это действительно сложные файлы, радикально отличающиеся для разных окружений.
ansible
├── ansible.cfg
├── inventories
│   ├── production
│   │   ├── group_vars
│   │   │   └── all
│   │   ├── hosts
│   │   └── templates
│   │       └── etl-server
│   │           └── application.yml
│   └── infrastructure
│       ├── group_vars
│       │   └── teamcity-server
│       └── hosts
...
Чтобы так делать, нужно использовать каталог (а не файл, который hostfile) Inventory, и определять inventory_ignore_extensions в ansible.cfg. Использовать такой шаблон, заданный в Inventory, можно так:
template:
  src: '{{ inventory_dir }}/templates/etl-server/application.yml'
  dest: '{{ etl_basedir }}/config/application.yml'
  owner: '{{ etl_user }}'
  backup: yes
Нужно быть осторожным, если Inventory содержит ключи да пароли. Ведь оно лежит в репозитории. Для публичных проектов нужно будет использовать Ansible Vault.
Ansible roles
Roles. Роли. Тут самое мясо. Тут команды, которые, собственно, что-то делают.
Начинается всё с подкаталога roles, а дальше идут каталоги, чьи имена соответствуют названиям ролей.
ansible
...
├── roles
│   ├── common
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   └── tasks
│   │       ├── common-firewall-disable.yml
│   │       ├── common-firewall.yml
│   │       ├── common-locales.yml
│   │       ├── common-ntp.yml
│   │       └── main.yml
│   ├── etl-deploy
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── etl-deploy-config.yml
│   │   │   ├── etl-deploy-jar.yml
│   │   │   └── main.yml
│   │   └── templates
│   │       └── application.yml.jinja2
│   ├── etl-provision
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── etl-provision.yml
│   │   │   └── main.yml
│   │   └── templates
│   │       └── service.jinja2
│   ├── java
│   │   ├── defaults
│   │   │   └── main.yml
│   │   └── tasks
│   │       ├── java-install.yml
│   │       └── main.yml
│   ├── mongodb-install
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── files
│   │   │   ├── logrotate.config
│   │   │   └── mongod.service
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── main.yml
│   │   │   ├── mongodb-configure.yml
│   │   │   ├── mongodb-install.yml
│   │   │   └── mongodb-logrotate.yml
│   │   └── templates
│   │       └── mongod.conf
│   ├── nginx
│   │   └── tasks
│   │       ├── main.yml
│   │       ├── nginx-configure.yml
│   │       └── nginx-install.yml
│   ├── spark
│   │   ├── defaults
│   │   │   └── main.yml
│   │   └── tasks
│   │       ├── main.yml
│   │       └── spark-install.yml
│   ├── teamcity-install
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── main.yml
│   │   │   ├── teamcity-dir.yml
│   │   │   ├── teamcity-download.yml
│   │   │   ├── teamcity-jdbc.yml
│   │   │   ├── teamcity-service.yml
│   │   │   ├── teamcity-tools.yml
│   │   │   └── teamcity-user.yml
│   │   └── templates
│   │       └── service
│   ├── teamcity-nginx
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── main.yml
│   │   │   └── teamcity-nginx.yml
│   │   └── templates
│   │       ├── config
│   │       └── websocket.conf
...
В роли мы объединяем таски, которые выполняют какой-то связанный набор настроек, на определённой группе хостов, в определённый момент жизненного цикла продукта, который можно переиспользовать для разных групп хостов. Получается какая-то роль common для настройки локалей, NTP, включения файервола. По роли на установку и настройку нужного софта: Java, Spark, MongoDB, всё, что душе угодно. В простейшем случае эти роли просто ставят нужные пакеты. В более сложных случаях нужна ещё хитрая подгонка по месту.
И роли на установку наших сервисов. Тут важен момент, когда роль нужна. Поэтому на каждый сервис получается, как правило, две роли. Одна делает provision: создаёт юзеров, каталоги, юнит systemd. Её для данного хоста достаточно запустить один раз, более ничего из этого на данном хосте меняться не будет. Другая роль делает deploy: копирует jarник/бинарник сервиса, настраивает конфиг по шаблону, перезапускает сервис. Её нужно запускать при каждом деплое, когда код сервиса у нас изменился. Раз делать разные вещи нужно в разное время и с разной периодичностью, это должны быть разные роли.
Внутри роли у нас есть дефолтные значения переменных, в defaults/main.yml. Это можно рассматривать как документацию к роли. Тут перечислены все переменные, которые роль использует. Ну и заданы дефолтные значения, чтобы роль работала. Более правильные конкретные значения следует переопределять в Inventory.
Так как переменные задаются в Yaml, есть соблазн развешать иерархию имён в виде вложенных объектов с именованными свойствами. Но в Ансибле так не принято. Потому что в Ансибл не завезли нормального наследования/переопределения объектов. Поведение по умолчанию: объявление объекта уровнем выше полностью затирает все вложенные определения, и все многочисленные свойства нужно определять заново. Можно сделать, чтобы наоборот, все свойства всех объектов всегда мержились. Но тогда нельзя убрать свойство, определённое где-то ранее. В общем, лучше с этим не связываться, и задавать переменные как независимые примитивные значения, в худшем случае — списки. А для разделения пространств имён использовать префиксы через подчёркивание.
Далее в роли есть шаблоны, в templates, и файлы, в files. Разница в том, что файлы пересылаются на удалённый хост как есть, например, таском copy, а шаблоны сначала прогоняются через Jinja2, таском template. Соответственно, эти таски и ищут по умолчанию файлы и шаблоны своей роли в этих самых каталогах.
В tasks/main.yml у нас определяются таски. Но товарищ Бланка рекомендует в main.yml сами таски не писать, а инклудить другие файлы с тасками, проставляя при этом теги.
---
- include: 'mongodb-install.yml'
  tags:
    - mongodb
    - mongodb:install
- include: 'mongodb-configure.yml'
  tags:
    - mongodb
    - mongodb:configure
- include: 'mongodb-logrotate.yml'
  tags:
    - mongodb
    - mongodb:logrotate
Правда, в Ansible 2.4 простой как пробка include задепрекейтили в пользу статических и динамических инклудов. Похоже, теперь нужно использовать import_tasks.
Теги нужны, чтобы выполнить роль частично. Это бывает нужно. Это позволяет здорово сэкономить время. В идеале, конечно, нужно бы, для набора задач, которые запускаются отдельно, выделить отдельную роль, и отдельный плейбук. Но отдельную роль нельзя запустить из командной строки.
Допустим, известно, что нужно поправить конфиг грешной Монги. Но точно неизвестно, как именно. Приходится пользоваться методом научного тыка, и совершать несколько итераций, экспериментируя на одном хосте. Можно делать всё руками, а потом не забыть перенести изменения в Ансибл. Можно делать всё в Ансибле, но тогда, чтобы пропустить этапы установки MongoDB, а выполнить только замену конфига на конкретном хосте, можно задать теги и хост:
$ ansible-playbook -i inventories/production mongo-install.yml -t mongodb:configure -l mongo1
Ещё в ролях есть handlers. Это такие таски, которые запускаются только один раз после успешного выполнения других тасков, если эти таски что-то изменили. Жизненно необходимая вещь для перезапуска сервисов, если менялись бинарники или конфиги. И неперезапуска, если не менялись.
Полезно, кстати, последним таском в роли добавить команду валидации этих самых конфигов. Например, для nginx это может выглядеть так:
---
- name: copy nginx configuration
  template:
    src: 'config'
    dest: '/etc/nginx/sites-available/{{ teamcity_nginx_site_config }}'
  notify: restart nginx

- name: disable default nginx configuration
  file:
    path: '/etc/nginx/sites-enabled/default'
    state: absent
  notify: restart nginx

- name: enable nginx configuration
  file:
    src: '/etc/nginx/sites-available/{{ teamcity_nginx_site_config }}'
    dest: '/etc/nginx/sites-enabled/{{ teamcity_nginx_site_config }}'
    state: link
  notify: restart nginx

- name: validate nginx configuration
  command: '/usr/sbin/nginx -t'
  changed_when: false
Тогда этот хэндлер перезапустит nginx, только если конфигурация изменилась, и она правильная:
---
- name: restart nginx
  service:
    name: nginx
    state: restarted
Ansible playbook
Playbooks. Плейбуки. Игровые книжки. Это Yaml файл, который объединяет Inventory, точнее группы хостов, где это должно играться, с ролями, то есть тем, что должно играться.
Получается, на каждый независимый шаг развёртывания всей системы создаётся свой плейбук. Их получается довольно много, но меньше, чем ролей. На каждую группу серверов, которую можно (или нужно) настраивать отдельно — свой плейбук. На каждый наш сервис: пара плейбуков — для provision и deploy. Собственно, разработчикам нужны только deploy плейбуки, чтобы запускать их, когда меняется код. А остальное запускается лишь один раз, либо когда появляется новый сервер.
Сам плейбук прост: это просто связь групп и ролей. Если у вас одно единственное Inventory, можно даже сделать файл исполняемым и снабдить shebangом.
#!/usr/bin/env ansible-playbook
---
- name: Provision MongoDB server
  hosts: mongodb-server
  roles:
    - common
    - mongodb-install
Можно поместить плейбуки в подкаталог playbooks, но нам пока хватало их россыпи прямо в каталоге ansible. Чтобы не запутаться мы делаем Makefile с самыми частыми вариантами запуска ansible-playbook.
Если плейбук завалился с ошибкой, Ансибл создаёт .retry файл рядом с файлом плейбука, куда записывает те хосты, где выполнение завершилось с ошибкой. Чтобы можно было сделать ansible-playbook playbook.yml --limit @playbook.retry. Не забудьте добавить *.retry в .gitignore.
Ansible playbooks
Вот так и живём с Ansible. Успешно.
Пользы от Ansible много. Это документация того, как должна развёртываться система. Грамотно написанные роли легко читаются, и из них вполне можно расшифровать конкретные команды, если, не дай бог, придётся настраивать вручную. Это работающие скрипты, которые с весьма высокой вероятностью приведут нужный набор хостов в нужное состояние. В случае stateless сервисов можно позволить себе роскошь безвозвратно потерять парочку серверов, ибо поднять новые точно такие же с помощью Ansible — дело пары минут.
Сейчас придётся делать микросервисы, запускаемые в облаке. Чувствую, что придётся изрядно переосмыслить роль Ansible во всём этом. Возможно, понадобится универсальная роль для деплоя любого сервиса. Возможно, эту роль нужно будет запускать не на каком-то удалённом хосте, а локально, чтобы управлять облаками через их API. Есть у Ансибла модули и для этого.
Ansible usage
P.S. Не факт, что следующая статья будет про букву «B».