2018-09-15

О почте

Бывает так.
Где-то на выделенном сервере работает тщательно настроенный Postfix. Через него ваше приложение, запущенное на этом же сервере, отправляет письма.
И SPF запись настроена. Она говорит, что с IP адреса нашего сервера действительно разрешается отправлять письма от нашего домена.
$ dig +noall +question +answer txt example.ru
;example.ru.              IN      TXT
example.ru.       28800   IN      TXT     "v=spf1 +a +mx include:_spf.yandex.net ~all"
И DKIM настроен. Письма подписываются, а ключ для проверки подписи тоже лежит в DNS.
$ dig +noall +question +answer txt mail._domainkey.example.ru
;mail._domainkey.example.ru. IN   TXT
mail._domainkey.example.ru. 28800 IN TXT  "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDTFcJLY5SlBNwtTqCHx5VHZZBnap4Z75T9Jb6g/JpNcwu3sMdBPmt8zQsxNflBXmovlBzowa4rAwu0MCk1HgZHt+5Ohm+qRscXTrP19XAcV+DA6ZVXekRNwoY7K+jwn3VJaUpEn3BK171M3gUPQ1G4sRDwh0A+S5ZEetH7V3SjmwIDAQAB;"
И даже все предыдущие письма, которые мы сами себе отправляли в Gmail, содержат заголовки, подтверждающие, что все проверки проходят успешно.
Authentication-Results: mx.google.com;
 dkim=pass [email protected];
 spf=pass (google.com: domain of [email protected] designates 1.2.3.4 as permitted sender)
 [email protected]
Но вот в один прекрасный день мы видим в логах Postfix вот такое:
421-4.7.0 Our system has detected an unusual rate of unsolicited mail originating from your IP address. To protect our users from spam, mail sent from your IP address has been temporarily rate limited. Please visit https://support.google.com/mail/?p=UnsolicitedRateLimitError to review our Bulk Email Senders Guidelines. gsmtp (in reply to end of DATA command)
Google отказывается (временно) принимать письма, адресованные пользователям Gmail, когда они отправляются с нашего сервера.
Ну да, мы слали подтверждения регистрации. Да, это очень похожие письма. Да, это были десятки похожих писем. Но отправлялись они не все скопом, а по мере регистрации пользователей.
Что мы можем сделать ещё, чтобы удовлетворить Google? Давайте внимательно почитаем, что нам предлагают по указанной ссылке.
Gmail
Читаем внимательно, обращая внимание на каждый пункт.
Используйте Postmaster Tools.
Теоретически, Postmaster Tools — это такая веб консолька, где вы видите статистику получения почты Гуглом из вашего домена, и можете увидеть истинную причину ошибки. На практике я им не пользовался. Потому что там было пусто. Видимо, сильно мало писем мы рассылаем, чтобы там была хоть какая-то статистика.
Всегда отправляйте письма определенной категории с одного адреса.
Ну как бы у нас одна категория: письма подтверждения регистрации. И отправляются с одного адреса.
Не смешивайте разнотипный контент в одном сообщении.
Не такие уж и большие у нас письма, чтобы там появилось более одного типа контента.
Отправляйте сообщения с одного IP-адреса.
Сервер у нас один, так что IP адрес тоже один.
Убедитесь, что для ваших IP-адресов существуют действительные обратные записи DNS, указывающие на ваш домен.
Уже теплее.
DNS, как вы знаете, предназначен для преобразования символических (доменных) имён, типа "gmail.com", в IP адреса, типа "216.58.207.37" или "2a00:1450:4001:821::2005".
$ dig +noall +question +answer a gmail.com
;gmail.com.                     IN      A
gmail.com.              260     IN      A       216.58.207.37
$ dig +noall +question +answer aaaa gmail.com
;gmail.com.                     IN      AAAA
gmail.com.              55      IN      AAAA    2a00:1450:4001:821::2005
Но существует и обратное преобразование. IP адресу может быть сопоставлено некоторое каноничное доменное имя.
$ dig +noall +question +answer -x 216.58.207.37
;37.207.58.216.in-addr.arpa.    IN      PTR
37.207.58.216.in-addr.arpa. 86378 IN    PTR     fra16s24-in-f5.1e100.net.
$ dig +noall +question +answer -x 2a00:1450:4001:821::2005
;5.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.1.2.8.0.1.0.0.4.0.5.4.1.0.0.a.2.ip6.arpa. IN PTR
5.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.1.2.8.0.1.0.0.4.0.5.4.1.0.0.a.2.ip6.arpa. 86400 IN PTR fra16s20-in-x05.1e100.net.
Как видите, это DNS записи типа PTR в поддоменах in-addr.arpa. и ip6.arpa.. Из IP адреса там формируется доменное имя задом напёред.
Если что, домен 1e100.net, принадлежит Гуглу.
От нас требуется, чтобы обратная зона IP адреса нашего сервера, во-первых, существовала, и во-вторых, соответствовала нашему домену, с которого мы отправляем почту.
Обратная зона неразрывно связана с IP адресом. А значит, изменить её можно там, где вам выдали IP адрес. То есть, у хостера. Ищите где-нибудь в панельке управления сервером, где поменять обратную зону.
Reverse DNS settings
Раньше бывали почтовые сервера, которые не принимали письма с IP адресов, если обратная зона не соответствовала домену отправителя. Подобную проверку и сейчас можно включить в Postfix. Google, вроде, не настолько жесток. Но вполне очевидно, что правильная обратная зона улучшает репутацию сервера. Ведь наличие обратной зоны говорит о том, что это не какой-то домашний сервер с динамическим IP адресом, а честный оплаченный хост у приличного хостера.
Reverse DNS Check
Используйте во всех письмах массовой рассылки один и тот же адрес в поле "От:".
Ну мы так и делаем.
Подписывайте сообщения ключом DKIM. Gmail принимает ключи длиной не менее 1024 бит.
Это мы тщательно настроили в прошлый раз.
Опубликуйте запись SPF.
Есть такая запись.
Опубликуйте политику DMARC.
А вот это мы поленились сделать. Кажется, пришла пора каждому приличному почтовому домену, помимо SPF и DKIM, иметь ещё и DMARC.
В простейшем случае хватит и такого:
$ dig +noall +question +answer txt _dmarc.example.ru
;_dmarc.example.ru.       IN      TXT
_dmarc.example.ru. 28800  IN      TXT     "v=DMARC1;p=none"
Мол, про DMARC мы знаем, вот вам эта грешная политика, но ничего специального не хотим, отвалите. Главное, чтобы нужная TXT запись была.
Если вы уверены, что SPF и DKIM работают правильно, и хотите получать хоть какие-то сведения о том, кто шлёт почту от вашего домена, имеет смысл усложнить до "v=DMARC1;p=reject;pct=100;rua=mailto:[email protected]". Тут, во-первых, письма с неправильным SPF или DKIM будут отбрасываться, а во-вторых, сводная статистика по письмам с вашего домена будет присылаться на указанный емейл. Там приходят зазипованные XML, которые имеет смысл автоматически парсить и где-то диаграммки рисовать. В принципе, это всё уже автоматизировано.
DKIM, SPF, DMARC
У IP-адреса отправителя должна быть запись PTR для выполнения обратного запроса DNS. Необходимо, чтобы он совпадал с IP-адресом, который получен путем прямого преобразования доменного имени, указанного в записи PTR.
Ага. Значит, нужно пойти чуть глубже, чтобы доменное имя обратной зоны IP адреса нашего сервера резолвилось в тот же IP адрес.
$ dig +noall +question +answer a gmail.com
;gmail.com.                     IN      A
gmail.com.              147     IN      A       216.58.207.69
$ dig +noall +question +answer -x 216.58.207.69
;69.207.58.216.in-addr.arpa.    IN      PTR
69.207.58.216.in-addr.arpa. 82607 IN    PTR     fra16s25-in-f5.1e100.net.
$ dig +noall +question +answer fra16s25-in-f5.1e100.net.
;fra16s25-in-f5.1e100.net.      IN      A
fra16s25-in-f5.1e100.net. 86400 IN      A       216.58.207.69
Это требование слабее предыдущего упоминания обратной зоны. Приличные провайдеры заводят домен и обратную зону даже для динамических IP адресов.
$ dig +noall +question +answer -x 188.232.179.78
;78.179.232.188.in-addr.arpa.   IN      PTR
78.179.232.188.in-addr.arpa. 2572 IN    PTR     dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru.
$ dig +noall +question +answer dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru
;dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru. IN A
Упс. Или нет.
Reverse DNS
Домен отправителя должен пройти проверку SPF или DKIM.
Да. Уже да.
Чтобы получатель мог быстро отказаться от подписки, не покидая Gmail, мы настоятельно рекомендуем добавить заголовок List-Unsubscribe.
Упоминаются два RFC. RFC 2369 описывает List-* заголовки писем, в частности, заголовок List-Unsubscribe. RFC 8058 описывает заголовок List-Unsubscribe-Post.
Теперь понятно, как работают эти ссылки "отписаться" в Gmail. Если вы делаете рассылки, то включите эти заголовки в ваши письма, и Google будет лучше их воспринимать. А у пользователей будет возможность быстро отписаться от рассылки. Вам будет приходить HTTP POST запрос.
List-Unsubscribe: <https://example.com/unsubscribe.html/opaque123456789>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Но вообще-то у нас не рассылки, а уведомления.
List-Unsubsribe
С помощью поля заголовка "Precedence: bulk" обозначена массовая рассылка.
Precedence — это нестандартный заголовок. Он пару раз упоминался в довольно старых RFC как выходящий из употребления, и в основном использовался для предотвращения зацикливания автоматических ответов в списках рассылки. А вот эта вот рекомендация использовать Precedence: bulk — уникальная для современного Gmail.
Чёрт его знает, как Gmail обрабатывает этот заголовок. Но, вероятно, если вы рассылаете много похожих писем, стоит этот заголовок выставить. Не жалко.
No Spam
Итак. Чтобы уменьшить вероятность проблем с Gmail (и другими получателями), нужно:
  • Иметь валидную SPF запись в домене.
  • Настроить и использовать DKIM.
  • Иметь хоть какую-нибудь политику DMARC для домена.
  • Определить обратную зону для IP вашего сервера. Убедиться, что домен обратной зоны снова резолвится в IP сервера.
  • Если вы действительно делаете рассылку, добавить заголовки List-Unsubscribe и List-Unsubscribe-Post. Дайте пользователю возможность быстро отписаться.
  • Если вы рассылаете много похожих писем, добавить заголовок Precedence: bulk.
  • Следить за заголовками From, To, Return-Path, Reply-To. Они должны отражать действительность.
  • Следить за телом сообщений, не забывайте включать текстовую версию, помимо HTML.
  • Быть честными с пользователями, пишите в письмах только то, что нужно, не надо рекламы.
Успехов.

2018-09-02

О Сочи

Сочи — это не город. Это — сборище санаториев, профилакториев, лечебных учереждений, дач, бывших дач, экспроприированных и превращённых в парки, посёлков, отелей, посёлков из отелей, горных черкесских аулов, пасек и виноградников, а также немного заказников, зажатые между Северо-Западным Кавказом и Чёрным морем узкой полоской от границы с Абхазией и на 120 километров на северо-запад.
Самая юго-восточная часть всего этого называется Адлер. Именно тут находится аэропорт. Именно тут построены те самые стадионы, которые вы видели по телевизору на трансляции Зимней Олимпиады 2014. Олимпиада принесла сюда ровные дороги, многоуровневые развязки и два модерновых железнодорожных вокзала. Плюс навороченная горнолыжная инфраструктура где-то в горах, где я не был.
Говорят, после Олимпиады Сочи стал круглогодичным курортом. Летом сюда едут на море. Зимой сюда едут в горы.
Далее на северо-запад у нас находятся: Хоста (Хостинский район), собственно Сочи (Центральный район)... Ну и считается, что Большой Сочи простирается куда-то до посёлка Лазаревка (Лазаревский район) и немного дальше.
Карта Большого Сочи
Из Адлера в Сочи идёт прямая автомобильная дорога: улица Ленина, тихо и незаметно переходящая в Курортный проспект. Это — единственная прямая дорога в Сочи. Далее на север — только горные серпантины. Вправо-вверх, влево-вверх, вправо-вверх, влево-вверх,... перевал, вправо-вниз, влево-вниз...
Автомобильное движение тут адовое. Я очень обрадовался, что оказался тут без авто. В движении на горных дорогах есть куча неявных нюансов. Тормозить двигателем на спуске. Пропускать фуры и автобусы на поворотах. Они не влезают по ширине и в повороте им нужны обе полосы, включая встречную. И их пропускают, и встречные, и попутные. Никаких обгонов. Зато автобус периодически пропускает скопившиеся за ним легковушки там, где дорога слегка расширяется.
Вдоль всего побережья проходит ещё и железная дорога. В единственном ровном месте, где её можно проложить. То есть по самой границе пляжа. Поэтому шутка Михаила Задорнова про железную дорогу на пляже — вовсе не шутка. Отдыхающим в большинстве санаториев придётся перебираться через автомобильную и железную дороги по подземным переходам, чтобы попасть на пляж. Единственное место, где железная дорога отходит от моря вглубь — это Адлер, тот самый его район, где стоят олимпийские стадионы, и Имеретинский район чуть южнее. Поэтому именно туда мы рванули, чтобы к морю.
Улица Ленина на Google Street View
Море есть. Море тёплое. В конце августа вода была +28°C. А пляж — галечный. Песка нет. Есть камни. Круглые, овальные... На них можно лежать. Но ходить лучше только в тапочках.
Эти камни тут везде. Ими прижимают листочки с рекламами туристических туров в киосках на каждом углу. Ими придавливают основания зонтиков от солнца, чтобы устойчивей стояли. Тащить с черноморского побережья красивые круглые камушки — бессмысленно. Ибо их тут — килотонны.
Камни берутся из гор. Тут нет этой чёрной мясистой почвы. Тут нет грязи. Тут, если голая земля — то это камни. Понятно, как тут растут сосны. Не очень понятно, как тут растёт всё остальное, включая настоящие пальмы и настоящий бамбук. Зато совсем-совсем нет пыли, которая в Сибири через два дня появляется на подоконниках при открытом окне и скапливается на балконах.
Земля в Сочи
По деньгам слетать в Сочи выйдет столько же, что слетать куда-нибудь в Европу. С пересадкой в Москве, естественно.
С жильём вариантов чудовищно много. Но меньшинство из них представлено на booking.com. Только ленивый не сдаёт квартиры или комнаты для туристов. В Адлере целые кварталы застроены двухэтажными домиками для туристов. Или пятиэтажными небольшими отелями с рестораном на первом этаже. Цены на жильё могут различаться в десятки раз, ведь тут есть и несколько штук Radisson. Но, в общем-то, гостиница уровня европейского Ibis, и стоить будет соответствующе.
Нам с жильём не повезло лишь по одному пункту. На первом этаже отеля оказался ресторан. И не просто ресторан, а ресторан со столиками на улице, где вечерами громко поют. А в соседнем отеле — тоже ресторан, где вечерами тоже поют. И они друг с другом соревнуются, кто поёт громче. И окна номера как раз выходят на них обоих (и на море, эх). Заснуть можно только плотно закрыв все окна. Хорошо, что эти ребята стабильно затыкались где-то за полчаса до полуночи.
Еда — очень дешёвая. Я ожидал, что будет дороже, как в Москве. Но нет, оказалось так же дёшево, как в Омске. Это если не ходить по барам и ресторанам, а питаться в столовках, которых на побережье южнее Олимпийского парка едва ли не больше, чем баров. За 300-700 рублей вполне можно пообедать втроём (два взрослых + ребёнок). Ну и ясен пень, всюду просто на улице — шашлыки и люля-кебаб. Впрочем, индейки тут нет, не выращивают.
Развлечения стоят по-разному. Но обычно в два-три раза дешевле, чем что-нибудь аналогичное в Европе.
На пляж вход свободный и бесплатный. За лежак, или зонтик, или качели-лежанки — почасовая оплата. За разные водные покатушки — плата отдельно.
Солнце встаёт из-за гор в шесть утра. Солнце садится в море в семь вечера. Пешеходная набережная с велодорожкой. Продают всякую детскую пластиковую фигню. Катят на велосипедах, самокатах, электросамокатах, трехколёсных велосипедах с электроприводом, рикшах, чёрт знает на чём. Художники рисуют. Гитаристы поют. Какие-то ребята каждый вечер играли дуэтом на гитаре и скрипке. Молодцы!
Вид с балкона отеля
Сходили разок с ребёнком в Сочи Парк. Впервые попал в парк развлечений, где плата за вход. Нехилая плата: больше полутора тысяч. Но не пожалел. Потом можно развлекаться целый день. На всех аттракционах. Без дополнительной платы. Там шикарные американские горки. С не менее шикарной длиннющей очередью на вход, но организованной а-ля-аэропорт накопителями зигзагом. А на цепочную карусель меня не пустили, потому что там ограничение на массу в 75 кг на посетителя.
Но аттракционы там — не самое интересное. Есть шоу. Научные, развлекательные, цирковые. Надо только заранее узнать, когда и где они будут проходить. Есть площадки под открытым небом. Просто лазалки. Прыгалки с музыкальными инструментами. Мини-лабиринты с кривыми зеркалами. Большая площадка с водой, насосами, брызгалками, водяными колёсами и прочей прелестью. На этих площадках, даже безо всяких аттракционов, ребёнок может спокойно провести полдня.
Позднее выяснилось, что в Москве, в Парке Горького, тоже есть подобные площадки, даже круче. С совершенно свободным доступом, и, соответственно, толпами орущих ребятишек. Москвичи и гости столицы, или просто, у кого есть полдня до вечернего самолёта, имейте в виду.
Лазалки в Сочи Парке
Разок выбрались в центр Сочи. Познакомились с общественным транспортом. Всё плохо.
Есть электрички. Но станции идут километров через пять. Это вам не метро. До ближайшего вокзала может понадобиться пройти несколько километров. Далековато.
Есть автобусы. Цена — нормальная. По району — 22 рубля. Между районами (из Адлера в Сочи) — в четыре раза дороже. Платить водителю на входе через переднюю дверь. Кондиционеры — есть. Расписание — тоже есть. Но оно не работает. Двагис утверждает то же, что и таблички на остановке, что автобус тут ходит три-четыре раза в час. На самом деле: один автобус в час-полтора. Поэтому — набит битком, и надо стоять. На остановках в центре встречаются табло, показывающие время прибытия ближайших автобусов, но они работают нестабильно, автобусы умудряются оттуда исчезать или появляться мимо данных на табло.
Есть море. Но по нему ходят только редкие прогулочные яхты, да многочисленные катера, катающие отдыхающих на «бананах» и парашютах. Морского транспортного сообщения — нет. Единственный морской порт — в центре Сочи.
Самый-самый центр Сочи
Доехали на автобусе до парка «Ривьера». Мы хотели туда сходить в дельфинарий. Даже купили билеты онлайн. Но в тот день поехать не смогли. Спасибо sochi.kassir.ru. Хоть их заявление на возврат билета, в формате doc, — совершенно дурацкое, но моё письмо, присланное за три часа до представления, они рассмотрели, и деньги вернули.
Попали в «Ривьеру» на другой день. Оказалось, что «сад бабочек» — это лишь одна комната в дельфинарии. С кучей дохлых бабочек на булавках и парочкой полудохлых ещё летающих. И весь остальной «Ривьера» оставил какое-то впечатление старого советского никому не нужного парка развлечений.
Прогулялись по Курортному проспекту. Сочи — не город. Даже когда идёшь по центральной улице, нет ощущения, что ты в городе. Слева — горы. Справа — проглядывает море. Вокруг — сплошная зелень. И где-то в этой зелени вылезают здания этих самых санаториев или торчит шпиль морского вокзала. Какие-то отдельные строения в горном лесу. Где город? Где гущи домов? Где простор (помимо морского)? Или это я, прирождённый житель равнин, ничего не понимаю?
Посетили Сочинский дендрарий. Милое местечко. Крутая дача конца XIX века, сохранившаяся до наших дней. Забирайтесь наверх на канатной дороге. Спускайтесь зигзагами, разглядывая растения и животных. Там есть страусы и лебеди. А в нижней части парка живёт куча мелких петухов, которые кукарекают, выпрашивая вкусняшки.
Снова самый центр Сочи
По всему Сочи продают экскурсии. Сотни киосков. Десятки экскурсий. Напоминает Израиль. Экскурсионные автобусы собирают туристов пару часов, объезжая полсочи. А потом куда-то везут. К вечеру развозят обратно.
Мы поехали на 33 водопада. Это — ущелье, где действительно имеется куча водопадов. Небольших. Где-то 17 из них доступны туристам по проложенным дорожкам. Ещё свозили посмотреть на крупнейший на побережье дольмен. Это такие древнючие погребальные сооружения в виде громадного каменного домика с дыркой.
Дорогу к водопадам устроили с мокрым развлечением. Туристов погрузили в старенькие ГАЗ-66 и, кажется, в Урал-375. И повезли прямо по руслу горной реки. В конце лета горные реки пересыхают, и есть только отдельные неглубокие протоки. Вот эти протоки и форсировали вездеходы на скорости явно больше оптимальной. Туристы тряслись и визжали от радости, когда их окатывало брызгами от водной преграды. По мнению моей дочи, это было веселее американских горок.
Вторая наша экскурсия — на гору Ахун, где построена башня, с которой видно весь Большой Сочи. Да, видно. Да, красиво. Виды как в сторону моря, так и в сторону гор — замечательные. И в Агурское ущелье. Водопадов там не было, ибо для водопадов — не сезон, горные реки пересыхают.
На обеих экскурсиях нам устраивали дегустации. Мёда, сыра, вина и чачи. В двух разных местах. Как будто сыр и вино тут не делает только ленивый. Сыр — обычный сыр. Адыгейский, копчёный, как обычно. Мёд — разный и своеобразный. Вино — тоже разное. Чача — это 65-градусный самогон на отходах виноделия. Просто крепкий самогон.
Про вино понарассказывали много интересного. Тут, в Сочи, растёт только Изабелла, остальные сорта винограда привозят, и делают из них вино. На кавказском застолье предпочитают белые вина, потому что от них меньше пьянеешь. А на кавказком застолье нужно выпить много: по стакану на тост. Сухие вина — более натуральные. В сладких принудительно останавливают процесс брожжения, и уже типа не то. Вино вполне можно разбавлять водой (не из крана, но хотя бы кипячёной), это меняет (и даже улучшает) его вкус. Сталин любил Киндзмараули, или Хванчкару.
Сталина тут до странного часто поминают. «Тут была дача Сталина». «Этот проспект проложили по приказу Сталина»...
Всё, что дают продегустировать, можно купить. И даже увезти домой. И даже на самолёте, если у вас есть багаж. Вино наливают в пластиковые бутылки.
Тут я впервые попробовал полусухое вино, которое мне понравилось. А то белое полусладкое, полторашку которого мы таки купили, оказалось сногсшибательным. Оказалось, что достаточно двух стаканов, чтобы им напиться.
Шайтан-машины
Аэропорт Сочи, который в Адлере, теперь занимает первое почётное место в моём рейтинге аэропортов. Он — не похож на аэропорт. Нет очередей на входе. Нет многочисленных стоек регистрации, они как-то аккуратно прячутся за пальмами в холле. Ненавязчивый досмотр. А после досмотра на посадку ты попадаешь в магазин. Не в тесные коридоры терминала, заставленные сиденьями, а в нормальный duty free магазин. И должен зигзагами через этот магазин пройти. Гениальный маркетинг!
Сиденья и выходы будут потом. Но снова совершенно естественно перемешиваясь с магазинами и кафе. Там даже есть маленькая детская площадка, между двумя магазинами игрушек. Всё-таки ребята с Кавказа любят и умеют продавать. Даже экскурсовод Армен, человек-не-умолкающий-ни-на-минуту, не просто рассказывал о достопримечательностях, но и нещадно, но ненавязчиво, рекламировал, что там можно купить.
В аэропорту
Сочи — хорошее место для отдыха. Не бюджетное, в силу расстояний (бо́льшая часть потраченных денег — авиабилеты). Но хоть виза не нужна. А вот кроме отдыха там делать нечего. Убери туристов — и весь смысл существования этих санаториев полностью исчезнет.

2018-08-11

О ГИС

ГИС — это Географическая Информационная Система. Именно поэтому 2ГИС так называется. Это и геоинформационная система, то есть карта, и Городская Информационная Система, то есть справочник организаций.
Но 2ГИС — это, так сказать, read-only GIS. Вы можете посмотреть, поискать, построить маршрут. Но ничего не можете менять.
В серьёзном взрослом мире нужно таки менять. Рисовать карты. Рисовать что-нибудь на картах. Типичный пример: OpenStreetMap. Это общенародная карта, где всякий может подрисовать свой гараж или тропинку в саду.
Но OSM собирает только картографические данные. А иногда нужно просто нарисовать что-то в привязке к местности. Я не геодезист и не архитектор, но, подозреваю, этим людям постоянно нужно что-то проектировать именно на карте. И для них существуют специальные GIS программы. Так же как для инженеров/чертёжников существуют CAD.
Хотел сказать, что единственной вменяемой свободной GIS является QGIS. Именно ею я и пользовался. Но Википедия говорит, что десятки их. Десятки только свободных. Но я всё равно рекомендую QGIS.
QGIS splashscreen
(На самом деле уже релизнулась версия 3.2, но во второй ветке сплешскрины красивее.)
Для чего вам может понадобиться ГИС? Почти для всего, где вам нужна будет карта. И надо на этой карте что-то нарисовать. Наложить треки, расставить точки, подсчитать расстояния. Да хотя бы редактировать OSM. Простые гугло/яндекс карты далеко не всё это могут.
Например, такая задача. Есть местность, где производятся некие инженерно-копательные работы. Вам, как программисту, нужно отобразить на карте план этих работ, дополнить всякими метками и графиками. В веб-приложении. И чтобы было сэкси.
Как бы не проблема. Есть Mapbox или Leaflet. Они могут отображать на своих слоях любой GeoJSON. Понятно, что GeoJSON можно просто сохранить в БД и выдавать по запросу. Или можно генерировать его из каких-то пространственных данных в другом формате.
Но где взять эти пространственные данные? Где взять эти точки, линии и многоугольники, с широтой и долготой по WGS 84? Если всё, что у вас есть — некий план, нарисованный в Paint. Хорошо хоть, в масштабе.
QGIS screenshot
Открываем QGIS и создаём новый проект...
Технически QGIS проект — это просто каталог слоёв. В виде файла с расширением .qgs в формате XML. А сами слои — это уже отдельные файлы, и не только файлы. Слои можно упорядочивать, группировать, скрывать или отображать, добавлять новые, удалять из проекта. Можно редактировать, но не все.
Условно можно выделить три типа слоёв.
Векторные слои. Самые важные, ибо вся работа, как правило, делается в них. Эти самые точки, линии да многоугольники — есть векторные сущности.
Векторные данные можно загружать из БД. Из PostgreSQL, возможно с PostGIS, хотя он умеет пространственные данные и просто из коробки. Из SpatiaLite — геопространственного расширения SQLite.
Векторные данные можно брать из файлов. Тот же GeoJSON сгодится. Треки в виде GPX файлов. Можно даже импортировать настоящие автокадовые чертежи в DWG. Только сначала нужно их конвертировать в DXF.
Если не связываться с базами данных, то самый ходовой формат для векторного слоя — так называемые Shapefiles. В одном Shapefile можно хранить только один тип данных: точки, линии или полигоны. QGIS умеет редактировать shapefiles в полном объеме.
Технически это на самом деле несколько файлов. .shp — бинарный файл с географическими координатами. .dbf — табличка dBase со свойствами наших географических примитивов. И ещё несколько для всяких связей, индексов и описания используемой системы координат.
Да, добавляешь в проект десяток слоёв, и вот уже в папочке проекта завалялась сотня странных файлов. А учитывая, что один и тот же файл можно умудриться включить в проект в виде разных слоёв, например, с разным отображением, то вообще кошмар.
Shapefile vs
Второй тип слоёв: растровые. Обычные картинки из пикселей. Их тоже можно натянуть на карту. Обычно предпочитают TIFF, потому что в него можно включить метаинформацию о тех же координатах. А вот рядом с PNG могут снова образоваться вспомогательные файлы.
Третий тип слоёв: тайловые подложки. Если рисуем на местности, нужно эту местность для начала как-то представить. И, с помощью плагина OpenLayers мы можем добавить в наш проект слой с картинкой из OpenStreetMap, или карт Google, или даже спутниковых снимков от Bing или снова от Google. Это будет работать как обычные веб-карты (только медленнее), тайлы будут качаться из интернетов.
Шаг нулевой выполнен. Добавлен нижний слой с тайлами из OpenStreetMap.
Плагины. QGIS написан на PyQt. И плагины к нему пишутся на Python. Тысячи их. Собственно, весь QGIS — это сборище плагинов.
Шаг первый. Мы нашли местность. Теперь на эту местность нужно натянуть план из растровой картинки. Это называется Georeferencing.
Дело в том, что естественными координатами растрового изображения являются координаты его пикселей. Столько-то пикселей по горизонтали, столько-то по вертикали. Это ещё и целые числа. Это соотносится с нашими вещественными широтой и долготой, которые в градусах глобуса, почти никак. Вот координаты в пикселях и нужно преобразовать в координаты в градусах на глобусе. И хорошо ещё, если обойдётся линейным преобразованием.
Тут нужно ещё помнить, что глобусы тоже бывают разные. Но, слава богу, в этих наших веб-картах, с проекцией Меркатора, принята система координат под названием EPSG:3857 (она же "WGS 84 / Pseudo-Mercator"). Только, внимание, единица изменения тут — метры! А если нужны градусы (а они нужны), но нужны другие координаты: EPSG:4326 (они же просто "WGS 84"). Причём в GeoJSON принято сию систему координат (CRS — Coordinate Reference System) называть urn:ogc:def:crs:OGC:1.3:CRS84.
У каждого слоя может быть своя система координат. Слава богу, QGIS умеет самостоятельно всё пересчитывать на лету. Постарайтесь не запутаться. И помните, что первая координата: X — это долгота (longitude), потому что по горизонтали. А вторая координата: Y — это широта (latitude), потому что по вертикали.
Поехали дальше. Нужно сделать georeferencing растровой картинки. Смысл в том, чтобы сопоставить некоторые референсные точки на картинке (в пикселях) с точками на карте (в широте и долготе). Двух точек достаточно для линейного преобразования. Но лучше расставить больше точек, чтобы сгладить погрешности, да и попробовать что-нибудь нелинейное. Как правило стоит обозначить что-нибудь по углам картинки, и ещё что-нибудь в середине.
Как нам это сделать? Я нашёл аж три способа.
Номер раз. Стандартный плагин Georeferencer GDAL. Eго не нужно скачивать, достаточно просто включить. Даёт, пожалуй, наиболее точный результат. При точных входных данных, конечно. Умеет красивые нелинейные преобразования. Если у вас на картинке есть линии, которые должны быть соответствующими линиями на карте, у вас есть неплохой шанс точно натянуть картинку на эти линии.
Алгоритм работы такой. В отдельном окошке загружаем картинку. Затем тыкаем на точку картинки и задаём координаты этой точки на карте. Для поиска координат на карте пригодится другой стандартный плагин Coordinate Capture. Помните, X — это долгота, а Y — это широта.
Georeferencing points
Когда мы натыкали достаточно точек, и сохранили их в отдельном файлике для этой картинки, можно попробовать выгнуть картинку, сохранить выгнутую, и добавить её слоем в наш проект. Можно выбрать несколько видов трансформации. Мне больше всего понравился "Projective". Это линейная трансформация, то есть она не изогнёт картинку дугой. Это линейная трансформация в трёх изменениях, то есть двумерная картинка обрабатывается матрицей три на три, и получается какая угодно наклонная проекция на плоскость карты. Получается неплохо.
Georeferencing transformations
Если результат нас не устраивает, удаляем слой из проекта, и возвращаемся к проставлению дополнительных опорных точек, или даже удалению лишних. Не очень интерактивненько.
Способ номер два. Плагин Freehand raster georeferencer. Как понятно из названия, всё придётся делать руками. Сюда сместить, тут наклонить, тут поднянуть. Прямо таскаете картинку (её лучше сделать полупрозрачной) по карте, пока не ляжет нормально. Получается быстро. Кривые картинки мостить удобнее. Но, конечно, никакой суперточности. Для нашего плана из Paint — самое то, пожалуй.
Freehand georeferencer
Способ номер три. Плагин Raster Bender. Плагин экспериментальный, так что не забудьте разрешить установку экспериментальных плагинов. Им я так и не воспользовался, извините. Но иконка зачотная.
Bender
Зато я воспользовался его ближайшим родственником: плагином Vector Bender.
Дело в том, что ещё мне понадобилось натянуть на карту чертёж в том самом DWG. То есть сделать georeferencing для векторных данных. И если для растра есть несколько вполне приличных способов, то для вектора всё как-то не очень. Из более-менее стандартных средств есть разве что Affine Transformations. Но там нужно матрицу трансформации тупо задать руками. А откуда взять цифры? Облазать полкарты с линейкой? И всё равно промазать?
Вот тут Vector Bender и пригодился. Он тоже экспериментальный, так что будьте осторожны. И он не то, чтобы сильно удобный в использовании.
В процессе создаётся целый новый слой. С прямыми. Которые обозначают векторы трансформации. То есть нужно указать, какая точка куда перемещается. А потом переместить. А потом ещё указать, если промахнулись.
В отличие от штатного растрового Georeferencer, Vector Bender определяет тип трансформации по количеству векторов. Линейная будет, если будет задано ровно два вектора перемещения. Это не удобно. А если задать больше, он будет править геометрию. Очень локально, пытаясь прилепить вот эту ближайшую точку вот сюда.
Ладно. Картинку на карту натянули. Где нам взять GeoJSON? Да просто обвести картинку. Создать новый векторый слой (в Shapefile), и натыкать в нём точек, если нужно пометить точки. Или нарисовать линий, если нужно обвести линии. А потом этот слой сохранить в GeoJSON.
Не забудьте указать EPSG:4326 при сохранении. И выставьте точность в шесть знаков после запятой. Квадриллионные доли градуса, которые предлагаются по умолчанию, вам не нужны.
Ну и всё. Извините, что не прикладываю скриншоты со своего QGISа. Ибо NDA.

2018-07-14

Об Астане

Из Астаны (тогда ещё Акмолы) я уехал в 1997. Потом, пока учился, проводил там лето. С тех пор продолжаю наезжать на несколько дней каждые год-два.
Астана выросла. Если верить карте, то она теперь аж по двадцать километров с севера на юг, и с запада на восток. Почти как Омск. Только в Омске через весь город идёт лишь две-три дороги вдоль Иртыша. А в Астане, так как строят заново и в пустой степи, есть много широких параллельных магистралей.
Астана выросла. Если верить Википедии, там уже второй год как больше миллиона жителей. Но на улицах это как-то незаметно. Да, в торговых да развлекательных центрах, возле популярно-туристических мест, особенно на день города, — толпы народу. Но просто на улицах как-то до странного пустынно. Или это потому что улицы широкие, и автобусы ходят часто? Или это потому что в эти дни днём было довольно жарко?
Генеральный план
Автобусы действительно ходят часто. Как и написано, каждые 10-15 минут. На многих остановках поближе к центру висят табло, где честно показывается, через сколько минут какой автобус на эту остановку подойдёт. Почти как табло в аэропортах и на вокзалах. И автобусы действительно подходят.
В автобусах можно расплатиться наличными, девяносто теньге со взрослого носа (где-то чуть меньше двадцати рублей). Расплатиться кондуктору. Кондукторы тут встречаются весьма колоритные. Они могут, как морские волки, стоять, ни за что не держась, на крутых поворотах. Они могут, как вежливые дворецкие, выяснять у пассажиров, не выходят ли они на следующей остановке, громогласно доносить эти сведения водителю, и чуть ли не самостоятельно предупредительно открывать двери перед тобой.
А вот водители автобусов явно брали уроки у омских маршрутчиков. Везут дрова. Резко ускоряются, резко тормозят, входят в повороты на скорости выше разумной. Подрезают друг друга на остановках, блокируют, и высаживают пассажиров в вонючий бок другого автобуса. У них что, KPI такой кривой, что им нельзя не торопиться? Хорошо хоть, что все эти непотребства они делают на выделенной автобусной полосе.
Кстати, а почему автобусы? Почему не электробусы? Это же модно, молодёжно и экологично. Дорого?
Небоскрёбы
Много потёртых, ушатанных, помятых, старых японских автомобилей. То ли тут требования к тех. состоянию автомобилей менее строгие. То ли какая-то особенность местного автострахования, что вмятины никого не волнуют.
От аэропорта куда-то в город строят то ли монорельс, то ли просто надземное метро. Собирались это соорудить ещё к Expo 2017, но начали строить только сейчас. Вдоль дороги возводят железобетонные Y-образные опоры, очень похожие на опоры метромоста в Омске. Строит китайский подрядчик, силами китайских рабочих, на деньги китайского инвестора, в Астане.
Строят много. Строят везде. Город всё дальше и дальше расползается по степи. Скоро будет поглощать многочисленные озёра и болота вокруг.
Новый пешеходный мост
Казалось, что после Байтерека и соответствующего бульвара с небоскрёбами по бокам, благодаря которым город существенно шагнул на левый берег Ишима, уже ничего не будет. Ан нет. К тому самому Expo 2017 построили не только выставочные павильоны, но и здоровенный торговый центр «MEGA Silk Way», и целый квартал больниц.
Скоро застроенный левый берег Ишима сравняется по площади с историческим правым берегом. А в середине этого левого берега есть квадратный километр ботанического сада. Целый квадратный километр, засаженный деревьями. Они ещё маленькие, но через несколько лет это может стать очень интересным местом для прогулок.
Действительно, строят не только министерства и жилые кварталы. Очень очень много новых больниц. Половина из них названы национальными («ұлттық») или именем действующего президента. Аэропорт, кстати, тоже именем президента теперь назван.
По всему городу начали появляться велодорожки. С забавными поднятыми наклонными урнами возле них. Типа чтобы можно было на ходу бяку выкинуть. Но велосипедистов что-то мало видно. Имеются парковки, где можно взять велосипед напрокат. Но я лишь раз видел, чтобы кто-то ими пользовался.
Скульптура
Мы попали на празднование двадцатилетия столицы. И день рождения президента заодно. Не очень заметно, чтобы это как-то особо затронуло горожан. Ну да, в пятничный выходной все пошли гулять по паркам. Как и в любой другой выходной. По отзывам, многие просто уехали из города на длинные выходные.
Зато на многих мало-мальски солидных площадях поставили сцены и были концерты. Мы попали лишь на сцену у Байтерека, где проходил фестиваль современной этнической музыки. Ребята из Грузии, с шикарным этническим акцентом, пели песни «про любовь» и «для женщин». По-грузински, под аккомпанемент бас-гитары, клавишных и ударных. Современная же, хоть и этническая, музыка.
Ещё мы сходили в павильон «Нұр Әлем», что остался после Expo. Это здание в виде гигантского стеклянного шара. С выемкой под муляжи ветрогенераторов на верхушке. Поэтому его называют «звездой смерти». Теперь там получился неплохой музей, посвящённый различным способам добывания энергии. Очень хороши интерактивные стенды, но их маловато.
Нур Алем снаружи
Я не устаю удивляться, как в современных музеях используют проекторы. В этот раз это были громадные анимированные пространства, где на всех стенах и потолке показывали историю птицы Самрук (которая как раз откладывает золотое яйцо-солнце в дерево Байтерек) и дракона Айдахара. Тюркская мифология — такая мифология.
Нур Алем внутри
Кажется, Астана становится настоящим городом. Не искусственно выращенной столицей, а настоящим. Живёт своей жизнью. Растёт. На какие деньги? А кто ж знает. Зато, чтобы найти ямы на дорогах, нужно очень постараться.
Страна становится суверенной не только, когда у неё появляется свой язык и валюта. Ещё появляются свои правила блокировки Интернета, свои платёжные системы, свои правила избрания президента и начисления пенсий. Даже роуминг становится таким же дорогим, как в Европе. В этом смысле Казахстан действительно теперь ощущается как другая страна, а не как площадка для запуска российских ракет.
Кстати, а вы знаете, что Казахстан собирается переходить на латиницу? При этом они обойдутся лишь апострофами над некоторыми буквами, и почти без диграфов. Имхо, с латиницей будет лучше. Будет сильно меньше проблем с казахофикацией. Наконец-то появятся красивые шрифты на вывесках, а не тот один-единственный уродливый шрифт, что везде вывешивают сейчас. Забавно, что название страны, «Қазақстан», по свежим правилам латиницы будет писаться как «Qazaqstan». Да, с буквы «Q».

2018-06-30

О коде

Мы — программисты. Мы пишем код.
Мы — девопсы. Или сисадмины. Или инженеры по инфраструктуре. Мы пишем конфиги.
Infrastructure
Существует множество правил и техник как правильно писать код. От паттернов до SOLID. В общем, всё сводится к тому, чтобы избегать дублирования кода, переиспользовать код, делать код таким, чтобы его можно было переиспользовать без изменения самого кода.
В результате, хорошо написанный код гибко настраивается, чтобы его можно было применить в большем количестве случаев. А параметры настройки часто пишутся в файлах конфигурации, конфигах.
Если нужно ещё больше гибкости, то встраивают какой-нибудь язык программирования. Часто интерпретируемый. (Привет, JavaScript. Привет, Lua.) И, помимо основного кода, у нас появляются скрипты.
Например, игры. Со времён Quake (и даже раньше) у любой серьёзной игры есть движок. Это — код, написанный программистами. И ресурсы. Это — конфигурация, написанная гейм-дизайнерами. Со своими скриптами на QuakeC. И именно ресурсы делают игру игрой. Движок лишь предоставляет техническую возможность.
Где проходит граница между кодом и конфигом?
Если мы сделали классный и расширяемый код, не пора ли сделать классные, удобные и расширяемые конфиги к нему?
Есть ли паттерны и SOLID для конфигов?
Очевидно, есть. Только никто ещё не написал про это книжку. Возможно, потому, что не существует конфигов «общего назначения».
Посмотрите, как организована конфигурация того же Nginx в Debian-based дистрибутивах. В каталоге /etc/nginx/sites-available, в отдельных файлах, имеются конфигурации виртуальных хостов. Чтобы включить виртуальный хост, нужно создать симлинк в каталог /etc/nginx/sites-enabled. Чтобы выключить, нужно убрать симлинк. Чтобы добавить новый виртуальный хост, достаточно добавить новый файл, и сказать Nginx перечитать конфиги. Не нужно править уже существующие конфиги. Имхо, это вполне очевидное следование заветам SOLID.
Причём со стороны самого Nginx тут нет ничего сверхъестественного. Он читает лишь один файл /etc/nginx/nginx.conf. Но в этом файле имеется несколько директив include. «include» является точкой расширения конфига. И позволяет применять паттерны. Как те, которые приняты в Debian.
А Ansible? Изначальная идея очень проста. Есть модули, которые делают что-то полезное. Которые написаны на настоящем языке программирования Python. И которые могут переиспользоваться. Мы пишем последовательность вызова модулей, с соответствующими параметрами. То, что в Ansible называется task. Пишем в YAML файле. То есть пишем конфиг.
Но мы тоже хотим переиспользовать и расширять эти YAML файлы. И есть много способов в Ansible для этого. Можно переопределить переменные (а переменные могут влиять на поведение). Повторяющиеся наборы задач выделяют в роли, которые можно распространять и переиспользовать. Наборы хостов, на которые можно применить эти роли, образуют inventory. Ну и инклюды тоже никто не отменял.
Кстати, о переменных. Переменные окружения — неотъемлемая особенность процессов Unix. И они, нежиданно, снова в ходу для этих наших контейнеров. Казалось бы, каким образом можно огранизовать набор ключей и значений? Но и тут у нас есть наследование и возможность переопределить.
А Terraform? Хоть тут вовсе не императивный, а вполне себе декларативный язык, о расширении и переиспользовании разработчики позаботились.
Снова переменные, которые можно задать и переопределить кучей разных способов. Переиспользуемые модули. Публичные репозитории готовых модулей. Workspaces, чтобы добавить ещё одно изменение, где этот код может быть переиспользован.
Reusable
Что бы ни считали разработчики, что с момента написания достаточно SOLIDного кода, когда все мыслимые настройки вынесены в файлы конфигурации, и их работа закончена, на самом деле потом начинается программирование конфигов. Программирование на XML (о, да, XSLT), на YAML, на HCL. (Только не на JSON, пожалуйста. Потому что в JSON нет комментариев.)
Конфиги — тоже код. Рано или поздно, так или иначе. И нужно предусмотреть способы расширения. И предложить шаблоны использования. Раз уж сами по себе языки разметки ничего подобного не умеют.

2018-06-12

О Terraform

Я освоил Terraform. Это ещё один замечательный инструмент от HashiCorp. Это те ребята, которые за immutable infrastructure. Которые сделали Vagrant, Consul, Packer и кучку других инструментов, популярных в альтернативной-Docker вселенной.
Terraform делает только одну задачу. И делает её хорошо. Задача эта: создать, собрать и настроить и ввергнуть во тьму ресурсы. Любые ресурсы, которые можно описать в виде набора свойств, понятных провайдеру этих самых ресурсов. В первую очередь речь идёт о ресурсах наших любимых вычислительных облаков. AWS и Azure. И множества других.
Terraform — это не есть единое API для всех облаков. За единым API попробуйте сходить к Kubernetes. Terraform — это единый способ описания ресурсов. Возможно, сразу в нескольких облаках. Но вот сами типы ресурсов, их имена и атрибуты, будут в каждом облаке свои. И в них придётся разобраться.
Зато описания ресурсов будут в текстовом виде. В файлах формата HCL (это нечто среднее между JSON и YAML). В вашей системе контроля версий. Это и называется Infrastructure as Code.
Terraform logo
Terraform написан на Go. Так что вся установка сводится к распаковке единственного бинарного файла из архива, прописыванию его в PATH и настройке автодополнения в вашей shell.
Поехали по терминологии.
Конфигурация. Configuration. Это набор *.tf файлов в текущем каталоге, то есть где вы запустили terraform. В этих файлах вы описываете то, что вам нужно создать и поддерживать. Не важно, как вы эти файлы назовёте. Не важно, в каком порядке в них всё опишете. Есть только небольшое соглашение, что должны быть файлы: main.tf — типа заглавные определения, variables.tf — все входные переменные, outputs.tf — все выходные переменные данной конфигурации.
Провайдеры. Providers. Плагины, отдельные бинарники, которые определяют весь остальной набор того, что можно задать в конфигурации. Они скачиваются при инициализации конфигурации. Инициализация делается командой terraform init.
Вот как может выглядеть описание провайдера для амазонового облака (AWS):
provider "aws" {
    version = "~> 1.20"
    region = "${var.aws_region}"
    profile = "${var.aws_profile}"
}
Конструкции с фигурными скобками — это interpolations. В данном случае берутся значения двух входных переменных.
Переменные. Variables. Входные значения для вашей конфигурации. Их надо явно описать:
variable "aws_profile" {}
variable "aws_region" {}
Переменные бывают трёх типов: строка, список, map. В данном случае тип не указан, и подразумевается строка. С поддержкой вложенных типов, типа списка mapов, всё настолько неопределённо, что лучше считать, что вложенных типов нет.
Значения для переменных, как и положено, можно передавать кучей разных способов. Через переменные окружения типа TF_VAR_aws_profile. Через параметры командной строки типа -var 'aws_profile="terraform"'. Через файлы *.tfvars, которые нужно явно передавать в командной строке: -var-file ../test.tfvars. Через файлы *.auto.tfvars в текущем каталоге, которые подгружаются автоматически. Понятное дело, есть правила переопределения переменных, а mapы даже мержатся.
Есть ещё выходные переменные. Это некоторые значения, которые являются выходом вашей конфигурации. Эти значения потом можно легко получить командой terraform output.
Ресурсы. Resources. То, ради чего всё это и затевается. Ресурсы, поддерживаемые провайдером, которые нужно создать, или модифицировать, или удалить.
У каждого ресурса есть тип, зависящий от провайдера, имя, в контексте данной конфигурации, и набор аргументов, которые нужно передать ресурсу. Также ресурс имеет некоторые атрибуты, это какие-то, возможно, вычисляемые значения, которые становятся известны, когда ресурс создан.
Вот как может выглядеть, например, создание S3 бакета:
resource "aws_s3_bucket" "frontend_bucket" {
    bucket = "${var.environment}-frontend"
    acl = "public-read"
    website {
        index_document = "index.html"
        error_document = "index.html"
    }
    tags {
        Name = "${var.deployment} Frontend"
        Environment = "${var.environment}"
        Deployment = "${var.deployment}"
    }
}
Атрибуты одних ресурсов могут быть указаны как interpolations при создании других ресурсов. Тогда Terraform понимает, что ресурсы имеют зависимость, и создаёт их в правильном порядке.
resource "aws_cloudfront_distribution" "frontend_s3_distribution" {
    origin {
        // вот ссылка на S3 bucket
        domain_name = "${aws_s3_bucket.frontend_bucket.bucket_domain_name}"
        origin_id = "S3-${var.environment}-frontend"
    }

    //... у CloudFront Distribution ооочень много аргументов
}
Чаще всего ресурсы один в один соответствуют тем ресурсам, что можно создать через API или в консоли управления облаком. Но иногда встречаются и «псевдоресурсы», которые представляют собой команды модификации «настоящих» ресурсов.
Например, Security Groups в AWS. Часто нужно, чтобы одна Security Group ссылалась на другую, а та ссылалась на эту. В консоли это проблем не вызывает, создаём обе, а потом модифицируем права доступа, чтобы они ссылались друг на друга.
Terraform так не умеет. Он считает, что все ресурсы должны быть созданы за один присест, без многошаговых модификаций, в порядке, согласно их зависимостям (а если нет зависимостей, то даже параллельно). А здесь получается циклическая зависимость, которая Terraform не устраивает.
Для решения проблемы придумали «псевдоресурс» aws_security_group_rule, который фактически является шагом модификации «настоящего» ресурса aws_security_group. Циклическая зависимость разрывается. Настоящие ресурсы создаются в несколько шагов.
// первая security group
resource "aws_security_group" "alb_security_group" {
    name        = "${var.environment}-alb-security-group"
    description = "Allows HTTPS from Anywhere into ALB"
    vpc_id      = "${var.vpc_id}"

    // здесь правила доступа прописаны внутри ресурса
    ingress {
        from_port   = 443
        to_port     = 443
        protocol    = "tcp"
        cidr_blocks = [ "0.0.0.0/0" ]
        ipv6_cidr_blocks = [ "::/0" ]
    }

    egress {
        from_port   = 0
        to_port     = 0
        protocol    = "-1"
        // тут ссылаемся на вторую security group
        security_groups = [ "${aws_security_group.ecs_cluster_security_group.id}" ]
    }
}

// вторая security group
resource "aws_security_group" "ecs_cluster_security_group" {
    // тут нет ссылок на первую security group
    name        = "${var.environment}-ecs-cluster-security-group"
    description = "Allows traffic from ALB"
    vpc_id      = "${var.vpc_id}"
}

// правило доступа для второй security group
resource "aws_security_group_rule" "allow_alb_for_ecs" {
    security_group_id = "${aws_security_group.ecs_cluster_security_group.id}"

    type = "ingress"
    from_port = 0
    to_port = 0
    protocol = "-1"
    // вот тут ссылка на первую security group
    source_security_group_id = "${aws_security_group.alb_security_group.id}"
}

// еще одно правило доступа для второй security group
resource "aws_security_group_rule" "allow_egress_for_ecs" {
    security_group_id = "${aws_security_group.ecs_cluster_security_group.id}"

    type = "egress"
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = [ "0.0.0.0/0" ]
    ipv6_cidr_blocks = [ "::/0" ]
}
Обратите внимание на передачу списков. Почему такие вольности, и почему кавычки обязательны, я не знаю.
    // одиночное значение можно обернуть в список
    list = [ "${var.single}" ]
    // список можно передать как список
    list = "${var.list}"
    // а можно обернуть в скобки
    list = [ "${var.list}" ]
    // а можно даже к списку добавить ещё значение
    list = [ "${var.list}", "${var.single}" ]
    // или даже объединить два списка
    list = [ "${var.list1}", "${var.list2}" ]
Источники данных. Data Sources. Провайдеры умеют не только создавать ресурсы, но и запрашивать атрибуты уже имеющихся ресурсов. Например, чтобы вставить какие-нибудь свойства в ваши ресурсы. Для этого и нужны источники данных.
Но есть и более интересные применения. Например, провайдер template позволяет читать локальные файлы-шаблоны, подставляя туда переменные. Синтаксис шаблонов полностью повторяет синтаксис interpolations. Что довольно убого. Циклов, например, туда не завезли.
Но есть очень интересный способ «добавить» циклы в шаблоны. Дело в том, что и у ресурсов, и у источников данных есть аргумент count. По умолчанию он равен одному, и создаётся ровно один ресурс или источник данных. (Если поставить нуль, то ничего создано не будет). А если поставить больше единицы, то будет создано несколько ресурсов или источников данных. И все атрибуты этих ресурсов или источников данных можно получить в виде списка. Это общая практика в Terraform: по спискам аргументов формировать списки ресурсов. А уж превратить список значений в тот же массив JSON — дело техники.
// определение провайдера
provider "template" {
    version = "~> 1.0"
}

data "template_file" "container_definition" {
    // читаем этот файл
    template = "${file("${path.module}/container-definition.json")}"
    // столько раз, сколько у нас задано контейнеров
    count = "${length(var.containers)}"

    vars {
        // тут доступ к элементу списка
        name = "${element(var.containers, count.index)}"
        // тут много списков...
        image = "${element(aws_ecr_repository.repository.*.repository_url, count.index)}"
        cpu = "${element(var.container_cpu_limits, count.index)}"
        memory = "${element(var.container_memory_limits, count.index)}"
        port = "${element(var.container_ports, count.index)}"
        region = "${var.region}"
        log_group = "${aws_cloudwatch_log_group.log_group.name}"
        log_stream_prefix = "${element(var.containers, count.index)}"
        environment = "${element(var.container_environments, count.index)}"
    }
}

resource "aws_ecs_task_definition" "task_definition" {
    family                   = "${var.environment}-${var.service}"
    // тут формируем json array из списка отрендеренных шаблонов
    container_definitions    = "[ ${join(",", data.template_file.container_definition.*.rendered)} ]"
    requires_compatibilities = ["FARGATE"]
    network_mode             = "awsvpc"
    cpu                      = "${var.cpu_limit}"
    memory                   = "${var.memory_limit}"
    execution_role_arn       = "${var.ecs_execution_role_arn}"
    task_role_arn            = "${var.ecs_execution_role_arn}"
}
Обратите внимание, для доступа ко всем переменным и атрибутам используется единый синтаксис с префиксами.
  • var.var_name — значение переменной
  • resource_type.resource_name.attribute_name — значение атрибута ресурса, определённого в данной конфигурации
  • resource_type.resource_name.*.attribute_name — список значений атрибутов ресурса, если count был больше единицы
  • data.data_source_type.data_source_name.attribute_name — значение атрибута источника данных
  • data.data_source_type.data_source_name.*.attribute_name — список значений атрибутов источника данных, если count был больше единицы
  • count.index — индекс текущей «итерации» «цикла», если count больше единицы
  • path.module — путь (в файловой системе) текущего модуля
  • module.module_name.module_output — значение выходной переменной модуля
Почти со всеми этими выражениями можно интерактивно поиграть, если выполнить команду terraform console.
Модули. Modules. Любая конфигурация в Terraform может рассматриваться как модуль. Текущая конфигурация, в текущем каталоге, называется root module. Любые другие конфигурации, в других каталогах файловой системы, из реестра модулей от HashiCorp, из репозитория на GitHub или Bitbucket, просто доступные по HTTP, можно подключить к текущей конфигурации и использовать.
При подключении модулю даётся имя. Под этим именем он будет здесь доступен. Нужно указать местоположение модуля. Одно и то же местоположение модуля можно подключать несколько раз под разными именами. Так что думайте об опубликованных конфигурациях как о классах, которые можно переиспользовать. А конкретное подключение здесь можно считать экземпляром класса. Наследования только нет :) Зато делегирование — пожалуйста. Модули могут использовать другие модули.
Кроме имени, модулю при подключении нужно задать аргументы. Это те самые входные переменные. Внутри модуля они будут использоваться как ${var.var_name}. А результат работы модуль предоставляет в виде выходных переменных. На них в данной конфигурации можно ссылаться как ${module.module_name.var_name}. В общем-то, почти аналогично использованию ресурсов.
// подключаем модуль и даём ему имя "vpc"
module "vpc" {
    // определение модуля берём из локальной ФС двумя уровнями выше
    source = "../../modules/vpc"

    // передаём аргументы
    environment = "${var.environment}"
    deployment = "${var.deployment}"
    region = "${var.aws_region}"
    availability_zones = "${var.aws_availability_zones}"

    // хардкодим аргументы
    vpc_cidr = "172.31.0.0/16"
    public_subnets_cidr = [ "172.31.64.0/24", "172.31.68.0/24", "172.31.69.0/24" ]
    public_subnets_ipv6_cidr = [ "2600:1f14:ee6:6300::/64", "2600:1f14:ee6:6301::/64", "2600:1f14:ee6:6302::/64" ]
    private_subnets_cidr = [ "172.31.65.0/24", "172.31.66.0/24", "172.31.67.0/24" ]
}

// вывод модуля транслируем в вывод данной конфигурации
output "public_subnet_ids" {
    // на вывод модуля ссылаемся так
    value = [ "${module.vpc.public_subnet_ids}" ]
}

output "private_subnet_ids" {
    value = [ "${module.vpc.private_subnet_ids}" ]
}
Резюмируем. Имеем набор *.tf файлов в текущем каталоге. Это — конфигурация. Или root module. Который может подключать другие модули из разных мест. Другие модули — это такие же *.tf файлы. Они принимают переменные и возвращают переменные. По ходу дела описывают ресурсы и используют источники данных.
Имеем полное декларативное описание того, что мы хотим получить. Как это работает?
Состояние. State. Второе, после конфигурации, ключевое понятие Terraform. Состояние хранит состояние ресурсов. Как оно обстоит на самом деле в этом облаке. По умолчанию состояние — это один большой (не сильно большой) JSON файл terraform.tfstate, который создаётся в текущем каталоге.
Этот файл состояния можно хранить в системе контроля версий. Но, если сильно много разработчиков будут править конфигурацию Terraform, состояние тоже будет часто меняться. И придётся постоянно править конфликты, не говоря уже о том, что не стоит забывать делать pull.
Поэтому лучше использовать remote state. Состояние можно хранить в Consul. А, в случае AWS, в S3. Тоже будет один файлик, но в облаке, и с версионированием. И будет всем доступен, и постоянно будет из облака дёргаться. Норм.
Поехали.
Сначала нашу конфигурацию нужно проинициализировать. Команда terraform init выкачивает плугины (провайдеры, бэкенды для remote state), модули (с GitHub, например), и сваливает это в скрытый подкаталог .terraform. Этот подкаталог не нужно хранить в системе контроля версий. Но он нужен для нормальной работы Terraform. Поэтому terraform init -input=false — обязательный шаг, чтобы запускать Terraform из CI.
Вторая команда для использования Terraform в обычной жизни: terraform apply. На самом деле она выполняет несколько шагов.
Шаг первый. Refresh. Terraform сравнивает текущее известное состояние с реальным состоянием ресурсов в облаке. Провайдер производит кучу запросов на чтение через облачное API. И состояние обновляется. При первом запуске состояние является пустым. Значит, обновлять нечего, и Terraform считает, что ни одного ресурса не существует.
Шаг второй. Plan. Terraform сравнивает текущее известное (и только что обновлённое) состояние с конфигурацией. Если в состоянии ресурса нет, а в конфигурации он есть, ресурс будет создан. Если в состоянии ресурс есть, а в конфигурации его нет, ресурс будет удалён. Если изменились аргументы ресурса, то, если это возможно, ресурс будет обновлён на месте. Или же ресурс будет удалён, а на его месте будет создан новый ресурс того же типа, но с новыми аргументами.
План будет представлен пользователю. С полным указанием того, что будет создано, удалено, или изменено. В той мере, насколько это известно до начала настоящего выполнения. (Идентификаторы ресурсов, например, как правило назначаются случайно, и становятся известны лишь после настоящего создания ресурса.) Надо ответить «yes», чтобы перейти к следующему шагу.
Шаг третий. Apply. Собственно, применение плана, созданного на предыдущем шаге. Согласно зависимостям. При этом снова изменяется состояние, туда записываются все настоящие свойства созданных ресурсов.
Ну вот и всё. Всё просто. Сверяемся, сравниваем, высчитываем разницу, накатываем изменения. В отличие от Ansible, сверка состояния делается один раз перед построением плана и для всей конфигурации сразу.
А теперь — нюансы.
Terraform — прост и упрям. И он не делает rollback.
К сожалению, в большинстве случаев создание даже одного ресурса — не атомарно. Даже создание S3 bucket — это с десяток разных вызовов, в основном, чтобы отдельно выяснить разные свойства этого бакета (get запросы). Если какой-то запрос не выполнился (а в моём случае наиболее частой причиной ошибок был недостаток прав на отдельные операции), Terraform считает, что создание ресурса провалилось.
Но в реальности ресурс таки мог создаться. Но это может не найти отражения в состоянии Terraform. И при повторном запуске может случиться повторная попытка создания ресурса, которая может сломаться теперь уже из-за конфликта имён. Или в облаке может оказаться более одного желаемого ресурса.
Кроме того, Terraform совсем не в курсе ресурсов, созданных автоматически при вызове API облака (пример: Elastic Network Interface в AWS создаются неявно). И не в курсе ресурсов, не описанных в конфигурации, но от которых могут зависеть его ресурсы (пример: Security Group в AWS не получится удалить, пока хоть кто-то её использует, но вот кто этот кто, Terraform знать не всегда может).
Но Terraform упрям. Так что после правки прав доступа, правки ошибок в конфигурации, и нескольких запусков terraform apply, облако таки неумолимо перейдёт в желаемое состояние. Плюс может остаться немного мусора — ненужных ресурсов, которые были созданы, но не были удалены. Мусор придётся подчистить ручками.
Потом, полагаю, придёт культура всё делать через Terraform, и сразу угадывать нужные права. И мусора будет меньше.
Можно ли перенести инфраструктуру, набитую ручками в консоли, в Terraform? Можно.
Пишете конфигурацию. Лучше добавлять один ресурс за одним. Делаете terraform import. По сути, вы сопоставляете имена ресурсов в конфигурации (resource_type.resource_name) с реальными идентификаторами существующих ресурсов (они разные для разных типов ресурсов). Terraform пытается прочитать атрибуты ресурсов и записать их в состояние. Делаете terraform plan и смотрите, не пытается ли Terraform что-то поправить. Если пытается, правите конфигурацию, и снова смотрите на план. В идеале Terraform скажет, что всё ок, и ничего править не будет. В случае похуже он всё-таки что-нибудь пересоздаст.
Аналогичным образом, только используя terraform refresh, можно привести конфигурацию в соответствие с теми изменениями, которые кто-то сделал своими шаловливыми ручками. (И ручки потом оторвать).
Импорт не всегда работает идеально. Совсем не работает для «псевдоресурсов», о которых я говорил ранее. Сложности возникают со сложными ресурсами, с множеством вложенных сущностей, вроде тех же Security Group или Route Table в AWS. Но после terraform apply и перетряхивания внутренностей ресурсов в угоду Terraform, всё устаканивается.
Рефакторинг. Рефакторинг конфигурации Terraform. Он возможен. Нужно только действовать аккуратно.
Переименование ресурса. Скорее всего Terraform предложит удалить старый ресурс, и создать новый, с новым именем. Если это неприемлемо, можно попробовать удалить ресурс из состояния командой terraform state rm, а потом сделать terraform import. Есть ещё специальная команда terraform state mv, которая вроде как специально предназначена для этого рефакторинга. Но я с помощью state mv как-то добился стабильного крэша Terraform. С тех пор остерегаюсь.
Разбиение одной большой конфигурации на несколько маленьких. На это есть несколько причин.
Причина первая. terraform apply выполняется не сильно быстро. И чем больше ресурсов имеется в конфигурации, тем медленнее. Ему же надо проверить состояние каждого ресурса, даже если в конфигурации этот ресурс и не менялся. Имеет смысл выделить части конфигурации исходя из частоты изменений и «охвата территории». Скажем, VPC вам придётся настраивать лишь один раз, и потом почти никогда не менять. ECS кластер вы будете создавать каждый раз, когда будет появляться новое окружение, и потом опять без изменений. А сервисы нужно подновлять каждый индивидуально почти при каждом деплое.
Причина вторая. Terraform пока плохо работает с версионируемыми ресурсами. Типичный пример: ECS Task Definition. Этот ресурс нельзя удалить, только пометить как неактивный. Этот ресурс нельзя изменить, только создать новую ревизию. Поэтому на каждый terraform apply будет «удалён» старый Task Definition, и создан новый Task Definition, даже если в конфигурации ничего не менялось. И обновлён ECS Service, который этот Task Definition использует.
Это приведёт к тому, что этот Service обновит и перезапустит свои таски. То есть произойдёт настоящий редеплой. Это хорошо, потому что можно делать редеплой ECS сервисов с помощью Terraform. Это плохо, потому что я не хочу редеплоить все два десятка моих сервисов одновременно по terraform apply одной большой конфигурации. Сервисы нужно выносить в отдельные конфигурации Terraform.
Хорошо. Создать ещё одну папочку с *.tf файлами рядом — не проблема. Перенести туда управление ресурсами с помощью terraform import тоже можно. Но ведь наши сервисы должны кое-что знать о кластере: имя кластера, те же Security Groups, ещё параметров по мелочи.
Конфигурации зависят от других конфигураций. Из более общих конфигураций нужно передать параметры более конкретным конфигурациям. Кластер должен знать о VPC, сервисы должны знать о кластере.
Можно передать всё, что нужно, горстку нужных идентификаторов, ручками. Как входные переменные нашей конфигурации. Будет работать. Если вы правильно скопируете эти идентификаторы.
Но есть более спортивный способ. Если у нас есть remote state, его можно подключить как источник данных, и прочитать выходные переменные совершенно другой конфигурации, чьё состояние мы подключили.
// наше состояние
terraform {
    backend "s3" {
        bucket = "my-lovely-terraform"
        key    = "environment/service/terraform.tfstate"
        region = "eu-west-1"
    }
}

// состояние региона
data "terraform_remote_state" "region" {
    backend = "s3"
    config {
        bucket = "my-lovely-terraform"
        // в другом файлике в S3
        key    = "environment/region/terraform.tfstate"
        region = "eu-west-1"
    }
}

// состояние кластера
data "terraform_remote_state" "cluster" {
    backend = "s3"
    config {
        bucket = "my-lovely-terraform"
        // в другом файлике в S3
        key    = "environment/cluster/terraform.tfstate"
        region = "eu-west-1"
    }
}

module "service" {
    source = "../../modules/ecs-service"

    // ссылаемся на выходные переменные других конфигураций
    cluster = "${data.terraform_remote_state.cluster.cluster_name}"
    ecs_execution_role_arn = "${data.terraform_remote_state.cluster.ecs_execution_role_arn}"
    vpc_id = "${data.terraform_remote_state.region.vpc_id}"
    subnet_ids = "${data.terraform_remote_state.region.private_subnet_ids}"
    security_group_ids = [ "${data.terraform_remote_state.cluster.cluster_security_group_id}", "${data.terraform_remote_state.region.security_group_ids}" ]

    //... в этом модуле у меня ещё много аргументов
}
Кроме модулей, у нас появляется возможность держать конфигурацию более-менее независимых кусков ресурсов отдельно. Это может быть удобно.
Terraform vs
Кажется, Terraform приживётся. Кажется, можно выкинуть Ansible, и оставить для управления контейнерами в AWS лишь terraform, docker и aws. Собственно, почему я пошёл в Terraform? Потому что Ansible, как оказалось, не умеет Fargate.
Terraform не заменяет Ansible, а дополняет его. Terraform создаёт ресурсы (виртуальные машины, где угодно), а Ansible настраивает их (через ssh). Другое дело, что если ресурсы — Docker контейнеры, то их и настраивать (изнутри) не нужно.