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».

2017-12-24

О типизации

Да начнётся срач!
Так получилось, что пару месяцев подряд я интенсивно кодил на Котлине. И учил студентов кодить на Яве.
Один разок я даже устроил воркшоп в стиле TDD, где к своему удивлению обнаружил, что добрая половина ява-кода в IntelliJ IDEA пишется по нажатию Alt+Enter. Это автоисправление ошибок: класс новый создать, который тут упоминается, но ещё отсутствует, метод новый добавить, и в нужные интерфейсы, и в нужные классы, когда этот метод впервые упоминается в тестах.
А другое автодополнение, по Alt+Space, но чаще просто выскакивающее само, после точки после имени переменной, позволяет особо не нагружать свою человеческую память тем, что умеют данные классы. Таким образом тоже «пишется» уйма кода.
А вот неделю назад пришлось расчехлить Питон. Надо было чуток подкрутить-поковырять проектик, который чуть ли не год пылился.
Ну и где моя магия автодополнений? Если что, у меня IDEA Ultimate, и все нужные плугины стоят.
Почему IDEA ничего не знает о существовании функции requests.compat.urljoin, пока я не напишу соответствующий импорт? Почему я, когда смотрю на __init__.py этого самого requests, тоже не вижу никаких compat? Откуда он вообще берётся? Почему в официальной документации requests ничего не сказано про compat? Почему таки использование этого urljoin нахваливают на StackOverflow? Его вообще можно использовать, или это секретное API, которое в любой момент может превратиться в тыкву?
Это лишь один, последний попавшийся, пример. Почему-то в Питоне всегда так. IDE лишь смутно предполагает, что тут сымпортировали, что тут передали, что с этим можно делать. Разработчику нужно либо действовать методом научного тыка, либо писать подробные тесты, либо очень хорошо знать ту систему, которую пишешь, и те библиотеки, которые используешь. Чтобы предполагать несколько больше, чем IDE. А если нужно быстро написать долбаный плугин к совершенно незнакомому фреймворку, вообще чувствуешь себя слепым котёнком. И остаётся только научный тык.
Сладывается у меня подозрение, что это всё из-за динамической типизации. Никто не знает, что тут пришло, лишь бы крякало. Если не крякает, это ваши проблемы, надо было выявить тестами.
Untyped Duck
Тут сразу возражают: ведь весь окружающий мир динамичен. Ведь нам в запросе приходит бог его знает какой JSON, и нам надо его обработать. И в языках с динамической типизацией это делается просто и естественно. А в языках со статической типизацией приходится сначала натягивать этот JSON на какой-то класс. Да и этот класс нужно сначала где-то объявить.
На самом деле, хочется видеть «линзу». Не важно, что там приходит. Но здесь и сейчас я хочу видеть объект с определёнными свойствами. Если нужных мне обязательных свойств нужного типа не пришло — это ошибка. Если пришло, натягиваем на объект, и поехали дальше уже со строгой типизацией.
Собственно, все приличные веб фреймворки так и поступают с входящим JSON. Вопрос только в том, как описать те свойства, которые нам интересны. Можно ява-бобами, можно котлиновыми дата-классами (обожаю их). В нашем секретном фреймворке мы вообще (почти) автоматически натягиваем динамический объект сообщения (а-ля Map) на (почти любой) интерфейс, описывающий ожидаемые свойства этого сообщения.
Написать интерфейс из нескольких геттеров, или написать дата-класс на три строчки — имхо, не большая плата за то, что во всех дальнейших операциях гарантируется наличие этих нужных свойств. Причём гарантия даётся ещё при написании кода. В рантайме сломаться может только маппинг динамического объекта на статическое описание. Ну мы же и не ожидали, что любой JSON на входе нам подойдёт?
Говорят, интерфейсы в Go как раз и делают то, что нужно. С одной стороны есть произвольные структуры. С другой стороны есть интерфейсы, описывающие ожидаемые методы. Если есть функции над структурой, реализующие эти методы, считается, что эта структура реализует этот интерфейс. Поэтому мы пишем интерфейс, указывая, что мы хотим. И пишем функции, указывающие, как из имеющихся данных получить то, что мы хотим.
Static Typing
Вообще вырисовывается три способа передачи развесистых данных в функцию. Передача динамического объекта, вроде мапы или словаря. Передача статического объекта, с заранее (на этапе компиляции) определённым набором полей. Ну и можно выделить именованные параметры, достаточно динамичный способ, тем не менее, задаваемый при написании кода, которым почему-то брезгуют многие языки. А ведь удобно, не зря же в современных API на JavaScript взялись передавать объекты с именованными полями, как единственный аргумент функции.
В языках с динамической типизацией легче передавать динамические объекты. Словари в Питоне, ассоциативные массивы в PHP, объекты (они там все динамические) в ЯваСкрипте. Создавать новые классы тут муторно и лень.
В языках со статической типизацией легче передавать статические объекты. Проще описать класс с нужными свойствами и сконструировать его экземпляр, чем собрать и разобрать какой-нибудь Map.
Наверное, это какой-то заговор. И вот эта лёгкость динамичного в динамичных языках и лёгкость статичного в статических языках как раз и приводит к тому, что языки остаются каждый в своей нише, и не идут навстречу друг к другу.
Duck Typing
А если нам нужны не данные, а поведение?
В Питоне есть такое понятие: file-like object. Попробуйте найти в официальной документации, какие именно методы должны быть у таких объектов. Конечно будет read(). Но ведь не только он?
Утиная типизация. Известно (по крайней мере ожидается), что это утка. Утка должна крякать. Но что такое «крякать»? Документация Питона не всегда даёт ответ на этот вопрос. Интерфейсы же, что Явы, что Гоу, дают чёткий ответ на этот вопрос.
И всё же, я предпочитаю статическую типизацию даже не из-за проверок компилятора, в конце концов тесты в любом случае нужно писать, а из-за того, что в случае, когда IDE знает точный тип, она может дать актуальные подсказки, которые действительно ускоряют работу. Если мерять эту работу не количеством набранных символов (что можно подсчитать по истории коммитов), а количеством нажатых клавиш (что можно подсчитать только локально в IDE).
Впрочем, тут есть и обратная сторона. Видел кучу разработчиков, которые вообще ни разу не открывали документацию того API, который они вовсю используют. Они просто ставят точку, смотрят список методов, который выдаёт IDE, выбирают наболее подходящий (по имени), и используют его. Конечно же они напарываются на неудачные названия методов, всяческие нюансы, вроде ограничений использования данных классов и методов, выбирают не оптимальные решения. Всё равно документацию читать надо. RTFM, как говорится.
Duck Typing
З.Ы. Не уверен, относится ли это к данной дискуссии, но утверждают, что с типами в Питоне вообще всё плохо.

2017-12-09

О Spring

Spring — это весна. Spring — это пружина. Spring — это родник. Springfield — это городок, где живут Симпсоны. Плюс ещё стопицот одноимённых городков в Соединённом Королевстве, Австралии и Соединённых Штатах.
А ещё есть Spring Framework. Фреймворк, который знают все явисты. Возникший когда-то как легковесная альтернатива Ынтырпрайзным ЯваБобам (EJB).
Spring Logo
Помню, как лет десять назад мы перелезали на Спринг с таких, ныне экзотичных вещей, как Apache Struts, или даже просто месива из сервлетов и JSP. Тогда надо было писать большущие простыни XMLя с описанием всех бинов. Уже тогда надо было подглядывать в исходники Спринга, чтобы понять, как туда воткнуть что-то нестандартное с точки зрения разработчиков фреймворка.
Сейчас в Спринге есть аннотации. И конфигурацию можно описывать Ява кодом. Или вообще не описывать, а просто помечать нужные классы как @Component.
Сейчас есть Spring Boot. Вообще крайне странная штуковина.
С одной стороны, он сильно упрощает начальное создание Спринг приложения. Это — POM артефакты, содержащие целые группы спринговых и внешних зависимостей, с тщательно подобранными (хочется в это верить) версиями, гарантированно работающими друг с другом. Это — автоматические настройщики, которые создают бины, исходя из набора свойств в application.yml или application.properties, и сканируют пакеты в поисках классов и методов помеченных хитрыми аннотациями, чтобы и их настроить. Это — плугины к Maven и Gradle, которые умеют собирать красивые суперджары, с внедрённым Jetty или Netty для веб приложений.
С другой стороны, Spring Boot весьма усложняет попытки уйти в сторону и сделать что-то непредусмотренное. Притащить другую версию библиотеки почти наверняка не выйдет, потому что у артефакта Spring Boot уже есть своя версия. Причём какая это версия, простого способа узнать нет, ибо там очень много транзитивных зависимостей. Чтобы воткнуть нестандартную конфигурацию, банально несколько подключений к разным MongoDB, придётся отключать автоконфигураторы. Какие именно отключать, придётся находить методом научного тыка, ибо нет простого списка этих конфигураторов с описанием того, за что они отвечают и что делают.
Spring Boot Logo
В тех случаях, когда наш любимый, акторный, но пока не очень человеколюбивый фреймворк использовать нельзя, мы берём Spring, Spring Boot и Kotlin. И нормально.
Точка входа в приложение, Application.kt, выглядит забавно.
@SpringBootApplication
@EnableAutoConfiguration(exclude=arrayOf(MongoAutoConfiguration::class, MongoDataAutoConfiguration::class))
@EnableScheduling
@EnableCaching
open class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}
main() тут получается в классе ApplicationKt. Но Спрингу и его Буту нужен ещё один класс, открытый, ибо станет бином. И вот этот класс и становится @SpringBootApplication.
Современный Спринг делает страшные вещи с вашими классами. То, что будет бином, т.е. любые классы, помеченные @SpringBootApplication, @Configuration, @Component, @Repository, @Service, @Controller, а также любые методы, помеченные @Bean, в Котлине должны быть open. Потому что вы (почти) никогда не получите на выходе (в IOC) именно ваш класс. Вы получите его наследника. Куда будут аккуратно засунуты все объявленные @Autowired и @Value.
Любой класс, куда вы навешаете эти аннотации, станет частью Спринга. Вам понадобятся спринговые зависимости, чтобы эти аннотации объявить. И если этот класс случайно окажется в пакете, где его найдёт автоконфигурация, он сразу окажется в IOC. А если не получится найти все нужные @Autowired и @Value, то приложение рухнет на старте.
Поэтому мы так не делаем. Мы стараемся делать сервисы и компоненты, которые вообще не зависят от Спринга. Все другие сервисы и компоненты, а также конфигурационные значения, которые нужны для работы этого сервиса, передаются в конструкторе. Как это и положено с нормальными объектами. Куча аргументов конструктора в Котлине — не проблема. Мы просто используем именованные аргументы.
Сами внешние сервисы и компоненты в аргументах конструктора представлены интерфейсами. Интерфейс легко замокать. И можно и нужно протестировать этот компонент как положено, юнит тестами. А ещё типы бинов удобнее представлять интерфейсами, тогда можно тихо и незаметно подменить реализацию. Кстати, Спринг вполне корректно различает генерик интерфейсы, с разными типами переменных типа.
class EntityInsertService(
    private val name: String = "",
    private val mongoOps: MongoOperations,
    private val collectionName: String,
    private val executor: Executor
) : IInsertService<Entity> {

    override fun insert(data: Entity) {
        //...
    }

    override fun flush() {
        //...
    }

}
Такие компоненты без аннотаций легко выносятся в (почти) независимые от Спринга библиотеки и переиспользуются между разными приложениями.
А в самом приложении они уже подключаются через объявление конфигурации.
@Configuration
open class InsertServiceConfiguration {

    @Value("\${insert.concurrency:2}")
    private var concurrency: Int = 2

    @Value("\${insert.collection:data}")
    private lateinit var collection: String

    @Autowired
    private lateinit var mongo: MongoOperations

    @Bean
    open fun insertService(): IInsertService<Entity> {
        val executor = Executors.newWorkStealingPool(concurrency)
        return EntityInsertService(
            name = "entityInsert",
            mongoOps = mongo,
            collectionName = collection,
            executor = executor
        )
    }

}
Kotlin Logo
Вот эти вот insert.concurrency — это проперти приложения. Как известно, их можно объявлять в application.properties. Но мы, конечно же, предпочитаем более сложновложенный application.yml. Из этих пропертей тоже можно конструировать бины, списки и даже мапы.
Вот вам нестандартная конфигурация с кучей Монг:
mongodb:
  connections:
    source:
      host: 172.31.22.180
      port: 27017
      database: project
      serverSelectionTimeout: 10000     # https://scalegrid.io/blog/understanding-mongodb-client-timeout-options/
      connectTimeout: 5000
      socketTimeout: 1000
    target:
      host: localhost
      port: 27017
      database: test
      serverSelectionTimeout: 10000
      connectTimeout: 5000
      socketTimeout: 1000
      writeConcern: acknowledged        # http://mongodb.github.io/mongo-java-driver/3
Можно, конечно, наваять @Configuration класс, куда внедрить все эти значения через @Value. Но мне понадобилось иметь произвольное количество таких подключений. Их все можно прочитать в мапу с помощью @ConfigurationProperties.
data class MongoConnectionProperties(
    var host: String = "localhost",
    var port: Int = 27017,
    var database: String = "test",
    var serverSelectionTimeout: Int = 0,
    var connectTimeout: Int = 5000,
    var socketTimeout: Int = 1000,
    var writeConcern: String = "acknowledged",
    var connectionsPerHost: Int = 10
) {
    val writeConcernValue: WriteConcern
        get() = WriteConcern.valueOf(writeConcern.toUpperCase())
}

data class MongoConnections(
    val connections: MutableMap<String, MongoConnectionProperties> = mutableMapOf()
) {
    operator fun get(name: String): MongoConnectionProperties
        = connections[name] ?: throw NoSuchElementException("No MongoDB connection mongodb.connections.$name")
}

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @ConfigurationProperties(prefix="mongodb")
    open fun allMongoProperties(): MongoConnections {
        return MongoConnections()
    }

}
Эта мапа должна быть мутабельной, а объекты должны быть настоящими ява бинами, с полным набором сеттеров, чтобы Спринг смог эту мапу заполнить. Поэтому тут в котлиновых датаклассах сплошные var и дефолтные значения. Для итогового набора свойств тоже нужно придумывать свой класс, нельзя просто вернуть коллекцию, потому что списки и мапы в качестве типа бина обрабатываются Спрингом по-особому: он пытается туда впихнуть все бины из контекста, подходящие по типу, а нам это совсем не нужно.
Из бина описания можно сделать настоящий объект MongoTemplate, который уже можно использовать.
@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @Scope("prototype")
    open fun mongoOperations(name: String): MongoOperations {
        val properties = allMongoProperties()[name]
        val options = MongoClientOptions.builder()
            .writeConcern(properties.writeConcernValue)
            .serverSelectionTimeout(properties.serverSelectionTimeout)
            .connectTimeout(properties.connectTimeout)
            .socketTimeout(properties.socketTimeout)
            .connectionsPerHost(properties.connectionsPerHost)
            .build()
        val dbFactory = SimpleMongoDbFactory(
            MongoClient(ServerAddress(properties.host, properties.port), options),
            properties.database)
        return MongoTemplate(dbFactory)
    }

}
Правда здесь у нас получается бин, который прототип, да ещё принимающий строку параметром. Получить такие бины из ApplicationContext труда не составляет. А вот @Autowired или @Inject для них уже не работают.
Поэтому приходится, если нужно, все эти прототипы создавать явно, и засовывать в синглетон коллекцию. Опять своего отдельного типа. Славься Котлин дата классами.
data class MongoOperationsMap(
    val mongos: Map<String, MongoOperations>
)

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    open fun allMongoOperations() : MongoOperationsMap {
        val map: MutableMap<String, MongoOperations> = mutableMapOf()
        for ((name, _) in allMongoProperties().connections) {
            map.put(name, mongoOperations(name))
        }
        return MongoOperationsMap(map)
    }

}
Ну а дальше этот наш дата класс можно автовайрить куда угодно.
Springfield
Иногда приходится бороться со Спрингом вообще, и со Спринг Бутом в частности. И хочется выкинуть этот Спринг нафиг. Со всеми его странными и несовместимыми обёртками вокруг обычного Монго драйвера.
Но как представишь, сколько вопросов возникнет без Спринга: Где создавать компоненты? Как их связывать друг с другом? Как автоматически и многопоточно подключаться к какой-нибудь очереди? Какой шедулер взять? Какой кэш взять и как его прикрутить?
И решаешь: пусть Спринг остаётся. Всё же он берёт на себя громадную кучу инфраструктурных проблем. А иногда с ним пободаться даже полезно.

2017-11-28

О Котлине

Тихо и незаметно вот уже второй настоящий коммерческий проект пишем с нуля и полностью на Котлине. И это ещё не считая всякой персональной мелочи под Андроид. Тихо и незаметно Котлин стал моим основным языком. Весь новый код по возможности стараюсь писать на Котлине.
А ещё три года назад всё выглядело не так однозначно. Была старушка Scala. Был странный Ceylon. И был подающий надежды, но с непонятным будущим, Kotlin.
В мире Scala не всё так однозначно. Typesafe стала Lightbend, и для всех их Scala продуктов заявлена поддержка отличного Java API. Scala перестал считать себя супер-языком, и стремится выжить в мире JVM.
Про Ceylon вообще ничего не слышно. Википедия говорит, новые версии выпускались стабильно, а в августе 2017 его передали в Eclipse Foundation.
А вот Kotlin расцвёл. Его настолько сильно продвигали в сторону Android разработки, что в результате его назначили first-class supported language для Android. И это хорошо. А ещё появился Kotlin/Native, который позиционируют для разработки под iOS. А ещё появляется всё больше чисто котлиновых библиотек, которые вовсю используют плюшки языка. А ещё в Котлине 1.1 добавили (пока экспериментальную) весьма интересную поддержку корутин. Минимальными изменениями в самом языке, без введения кучи новых ключевых слов, стало можно делать библиотеки, реализующие всякую асинхронщину вроде async/await, генераторов и всего такого.
В общем, пора писать на Котлине, если вы ещё не начали писать на Котлине. Котлин — прекрасен. Хотя не все с этим ещё согласны.
Kotlin logo (latest)
Объясняешь студентам: State — это такой класс, от которого нужно, чтобы его экземпляры были различными, в той степени, в которой нужно. Поэтому достаточно в нём иметь лишь одно строковое поле name. Но также нужно переопределить метод equals(), чтобы корректно сравнивать, и метод hashCode(), чтобы использовать в качестве ключа HashMap, и метод toString(), для удобства.
То есть в Яве нужно вот так:
public final class State {

    private final String name;

    public State(String name) {
        this.name = name;
    }

    public boolean equals(Object object) {
        if (this == object) return true;
        if (!(object instanceof State)) return false;
        if (!super.equals(object)) return false;

        State state = (State) object;

        if (name != null ? !name.equals(state.name) : state.name != null) return false;

        return true;
    }

    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    public String toString() {
        return "State{" +
                "name='" + name + '\'' +
                '}';
    }

}
А в Котлине для всего этого достаточно лишь:
data class State(
    private val name: String
)
Это дата классы. Они финальные. Из них нельзя построить красивую иерархию наследования. Но это и не нужно. Они идеально подходят для передачи и представления данных. Вот структуры и всё.
data class SampleData(
    val visibleReadOnly: String,
    private val invisibleReadOnly: String,
    var readWrite: String
) {
    val internalReadOnly = invisibleReadOnly.toUpperCase()
}

val instance = SampleData(
        visibleReadOnly = "A",
        invisibleReadOnly = "B",
        readWrite = "C"
)

instance.visibleReadOnly    // "A"
//instance.visibleReadOnly = "a"  // compilation failure
//instance.invisibleReadOnly  // compilation failure
instance.readWrite  // "C"
instance.readWrite = "c"
instance.internalReadOnly   // "B"
//instance.internalReadOnly = "b" // compilation failure
Большинство свойств нашего класса данных нужно задавать в конструкторе. Наличие именованных параметров позволяет делать конструкторы принимающими сколь угодно много аргументов. И это остаётся удобным. Просто всегда вызывайте конструкторы с именованными аргументами.
А ещё есть и дефолтные значения параметров. Это чертовски запутывает, если пользоваться старыми добрыми позиционными параметрами. Но если пользоваться именованными — опять всё ок.
Кроме параметров конструктора, можно задать и обычные проперти класса. Но тогда имеет смысл делать эти проперти чем-то производным от параметров конструктора.
Ещё можно и методы добавлять, не проблема. Но, как правило, это не нужно.
Если у вас на всех пропертях стоит val, вы получите честную иммутабельность. Задаёте все свойства в конструкторе, а потом можете их читать. Вполне солидно.
Но ещё есть метод copy(), который позволяет «мутировать» ваши иммутабельные дата классы. Он принимает тот же набор параметров, что и конструктор, только все с дефолтными значениями. И создаёт копию дата-объекта, с изменёнными свойствами.
val copy = instance.copy(visibleReadOnly = "AAA")
Именованные аргументы, да ещё и с дефолтными значениями — вообще хорошо. Иногда создаётся иллюзия, будто кодишь на Питоне.
Kotlin logo (previous)
Заметьте, как правило дата классы в Котлине не соответствуют соглашению Java Beans. В бинах подразумевается конструктор без аргументов. А в дата классах наоборот, все свойства принято передавать в конструкторе. В бинах подразумеваются сеттеры и геттеры. В дата классах, если хотите иммутабельность и ставите val, у вас будут только геттеры.
Не все сериализаторы умеют корректно работать с иммутабельными дата классами Котлина. Gson (или его котлиновая обёртка Kotson) — умеет. А вот в Spark соответствующий Encoder ещё не завезли.
Эмулировать Java Beans приходится как-то так:
data class JavaBean(
    var property1: String? = null,
    var property2: String? = null
)

val bean = JavaBean()
bean.property1 = bean.property2
Совсем пустой конструктор в дата классе нельзя. Приходится делать конструктор с дефолтными (нуловыми) значениями для всех свойств. Некрасиво страшно.
Kotlin logo (oldest)
Если посмотреть на котлиновые библиотеки, например, на Mockito-Kotlin, то окажется, что они интенсивно эксплуатируют две возможности Котлина: экстеншен функции и функциональные литералы.
Экстеншен функции — это штука, покраденная, вероятно, из C#. Ну или дальнейшее развитие идеи friend function из C++, если хотите. В общем, это совершенно левые функции, которые, тем не менее, ведут себя как методы объекта (любого нужного типа). Они имеют доступ к target объекту через this. Но они не имеют доступ к приватным свойствам и методам объекта. Технически это просто синтаксический сахар. Но удобный. И приятный.
target.doSomething()
//vs
doSomething(target)
Можно сделать так:
fun Long.asSeconds(): Instant = Instant.ofEpochSecond(this)
fun Long.asMillis(): Instant = Instant.ofEpochMilli(this)

val now = 1511759774L.asSeconds()
val nowMillis = 1511759774000L.asMillis()
«Отсутствующие» явно конструкторы в Котлине поначалу вызывают недоумение. Но потом как-то укладывается. Ведь, в конце концов, чаще всего конструктор используется, чтобы передать и сохранить параметры. А для этого вполне достаточно val/var объявлений в круглых скобках. Если нужно что-то посложнее, есть блок init { }, или даже возможность задать вторичные конструкторы.
Зато из-за такого упрощения конструкторов класс-исключение в Котлине записывается в одну строку.
class MyException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
Кстати, любители сhecked exceptions, ваше время прошло. Как был Ява единственным языком с этой критикуемой концепцией, так им и остался. В Kotlin нет checked exceptions. И нет ключевого слова throws. Впрочем, как вы заметили, checked exceptions нет и в свежих API, добавленных в Яву, например, в java.time.
В Котлине можно писать inline функции. Это позволяет проделывать удивительные, для Явы, фокусы. Например, узнавать тип переменной типа в генериках. Например, вот какая магия запрятана в mockito-kotlin:
inline fun <reified T : Any> mock(
      // тут много опциальных параметров
): T = Mockito.mock(T::class.java, withSettings(
      // сюда эти параметры передаются
))!!
Не пугайтесь !!. Это просто требование получить не null, и выкинуть NullPointerException, если получился всё же null.
Интересно, что тип T известен. Можно получить его класс, и передать в Mockito.
Используется это так:
val mock: MyInterface = mock()
Магия в том, что это inline функция. И reified можно использовать только с inline. Тело функции подставляется в место вызова. И вывод типов Котлина вполне может определить (а в данном примере тип задан весьма явно), что это за T здесь.
Android with Kotlin
Другая очень милая фича Котлина — функциональные литералы. Они сделали их гениально просто и красиво — просто фигурные скобки. Плюс возможность, если последний аргумент функции — функция, указать эти фигурные скобки после круглых скобок (а круглые вообще упустить, если нет других аргументов).
Поэтому можно написать вот такое:
class RunNotTooFrequently(
    val interval: Long
) {

    private var lastRun = 0L

    fun run(block: () -> Unit) {
        val now = System.currentTimeMillis()
        if ((now - lastRun) > interval) {
            lastRun = now
            block()
        }
    }

}

val runner = RunNotTooFrequently(2000)
runner.run {
    // do something
}
В стандартной библиотеке полным-полно таких функций, принимающих функции.
val stream = Files.newInputStream(Paths.get(logFile))

stream.bufferedReader(StandardCharsets.UTF_8).useLines { lines ->
    for (line in lines) {
        // process line
    }
}
В Котлине, в отличие от Явы, где тоже теперь есть всякие лямбды, функции — более полноправные члены языка. Можно просто фигачить функции в .kt файле, снова и питонячьем стиле, и это будет работать. Только будет не очень удобно, потому что все функции тогда будут объявлены в пакете, сам .kt файл никакого неймспейса не создаёт. Соответственно, и при импорте придётся их использовать по их имени. И беда-беда, если имена пересекутся.
А ещё в Котлине есть синтаксис для описания типа функции. Ну там, количество и типы аргументов, и тип возвращаемого значения. Это гораздо удобнее костылей в виде функциональных интерфейсов, что остался в Яве. Но из-за этого бывает, что при вызове какого-нибудь Ява-метода, который ожидает именно что функциональный интерфейс, иногда нужно функциональный литерал Котлина приводить к этому интерфейсу.
Интероперабилити с Явой просто отличный. Не помню серьёзных проблем, чтобы что-нибудь явовое вызвать из Котлина. Наоборот делать не приходилось.
Ну разве что один раз поймал багоособенность с varargs. В Яве что массив указать, что кучу аргументов в vararg метод передать — результат одинаков. В Котлине, если передаёшь массив, нужно не забыть звёздочку перед ним. Иначе передастся именно что один аргумент-массив, что просто невозможно в Яве. Впрочем, там был vararg на Object (each(Object... values)), что лишний раз подтверждает, что типы всё же нужно делать строже.
Kotlin island
Мы юзаем Котлин вместе со Spring и Spring Boot. Оно работает.
Местами получается забавно:
@SpringBootApplication
open class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}
Главное, не забывать делать классы и методы, которые помечены спринговыми аннотациями, open. Оказывается, современный Спринг очень любит заниматься кодогенерацией, наследовать ваши бедные классы и переопределять методы. По умолчанию в Котлине все классы и методы закрыты от переопределения. Спрингу нужно явно открывать. Есть, конечно, плагин к Gradle, который сделает это неявно. Но лишней магии лучше избегать.
Kotlin vs Java
Люблю Котлин за его лаконичность. Вот сколько строк на Яве займёт вот это?
val intVal = (doc.getValue("number") as? Number)?.toInt() ?: 0
Тут из некоего документа извлекается некое значение (Object, будь он неладен), которое может быть числом, целым или длинным. Нужно получить именно Int. А если не удалось, пусть будет нуль.