Текущий IP-адрес из консоли в MacOS

Мы продолжаем играться с докером и периодически возникает необходимость достучаться из одного контейнера до другого. По-правильному нужно поднимать docker network, но если лень и сетевая производительность не очень важна, то можно ходить из контейнера в контейнер по ip-адресу хост-машины, на которой запущен Docker for Mac.

Тут нас поджидают небольшие грабельки под названием DHCP — нет гарантии, что ip-адрес выданный ноутбуку сегодня не изменится завтра, и с вероятностью, близкой к 100%, мы получим другой ip, вернувшись в офис после выходных.

Очевидно, что ip-адрес нужно бы не хардкодить в скриптах, а получать его динамически. Для этого можно ввести в терминале эту команду:

$ ipconfig getifaddr en0

Если у нас активно несколько интерфейсов, например, есть WiFi и подключен Ethernet-шнурок, то просто меняем имя сетевого интерфейса:

$ ipconfig getifaddr en1

Самые умные и ленивые конечно же создадут алиас и запишут его в .bashrc/.zshrc.

Передача переменных окружения в Docker

Мы на работе немного угорели по микросервисам и всё заворачиваем в docker-контейнеры. Зачем — это тема для отдельного поста, сейчас об этом не будем. Просто поверьте, что оно нам действительно надо.

Что особенно удобно — легко поддерживается идентичность среды у разработчика на маке и винде и на серверах с линуксом. Но есть некоторые вещи, например подключения к БД и другим сервисам, ключи от всяких API и т.п. должны быть разными на разных окружениях.

Первым подходом было создание конфигурационных файлов и их подмена при сборке, всё это выглядело крайне костыльно и доставляло определённые неудобства.

Потом к нам пришли переменные окружения, их можно передавать при запуске контейнера поодиночке или все сразу, используя специальный файл (или несколько файлов).

$ docker run --rm --env DB_HOST=postgres_1.local --env-file=dev-env ubuntu env

В этом примере мы запускаем контейнер с убунтой и передаём в него пачку переменных из файла dev-env, а также явно указываем значение для переменной окружения DB_HOST. В результате в терминал выведется список всех переменных окружения внутри запущенного контейнер, после чего контейнер самоуничтожится (потому что он был запущен с ключом --rm).

Всё очень хорошо и красиво, но когда я начал переводить старый проект на эти новые рельсы, то наткнулся на несколько странное поведение. Проект написан на питоне, поэтому для чтения переменных окружения мы используем библиотеку environs. Она позволяет валидировать значения переменных во время чтения, и для одной такой переменной (пусть это будет тот же DB_HOST) был ограничен набор возможных значений.

Но для удобства разработки переменные брались из .env-файла, монтируемого прямо в контейнер и считывались прямо из него всё-той же библиотекой environs. Всё было прекрасно, пока не пошли деплоить на стейджинг. Тут приложение внезапно стало валлиться с ошибкой, что для DB_HOST передано недопустимое значение.

Проверяю env-файл на сервере — всё в нём правильно, ну ладно иду читать, что на самом деле пришло в контейнер и вижу, что значение переменной передано вместе с кавычками (то есть "postgres_1.local" вместо postgres_1.local). Мы давно привыкли всегда заключать значения переменных окружения в кавычки, потому что bash их игнорирует, но зато это позволяет нормально передать значения из нескольких слов, да и вообще, файлы лучше читаются. Но делать нечего, пришлось вычищать все кавычки из значений переменных, чтобы приложение смогло запуститься.

Но что ещё печальнее, такая внимательность к кавычкам у Докера проявляется только для переменных из env-файла, если переменная передаётся явно, через --env, то значение переменной преспокойно можно заключать в кавычки и в контейнер придут правильные данные.

По случаю этой ошибки была Issue на гитхабе, но что-то её закрыли ничего не починив.

Про сравнение объектов Immutable.js

Примерно год назад я начал переводить проект, над которым работаю в Charge.auto с жутко запущенного легаси на реакт-редакс-вот это всё. Для представления данных в сторе решил взять Immutable.js. Граблей было исхожено не одно поле, но на днях до меня наконец-то дошло, как разрешить одну из самых раздражающих меня проблем.

В приложении с сервера прилетает множество разных массивов данных, эти массивы раскладываются туда-сюда в сторе в List и дальше забираются в редакс-контейнерах и передаются как пропсы в компоненты. И вот тут начался праздник. Как правило, нужно выводить не весь список, а какую-то выборку по нему. Сначала я делал что-то типа

function mapStateToProps(store) {
  const items = store.get('beans').get('items');
  const activeItems = items.filter((item) => item.get('isActive'));

  return { activeItems };
}

Но почти сразу же я заметил, что компоненты постоянно обновляются, даже если данные не менялись. Причина проста — List.filter() создаёт новый инстанс отфильтрованного списка при каждом вызове, а в shouldComponentUpdate() использовалась просто строгое неравенство. На тот момент я ещё не до конца понимал всей кухни реакта и как писать реакт-компоненты правильно, поэтому просто стал передавать из контейнера в компонент полные списки, а фильтрацию и всё остально делать уже в компонентах. Но меня постоянно бесил тот факт, что приходится гонять туда-сюда жирные объекты, а компоненты получаются с излишней логикой.

Но вот на днях я нашёл целых два решения проблемы.

Во-первых, все объекты Immutable.js имеют метод .equals(), так что его и нужно использовать в shouldComponentUpdate()

shouldComponentUpdate(nextProps) {
  return !nextProps.activeItems.equals(this.props.activeItems);
}

Ну а во-вторых, можно пойти упарываться функциональщиной дальше и использовать мемоизацию (например, из библиотеки rambda)

function mapStateToProps(store) {
  const items = store.get('beans').get('items');
  const filterActive = R.memoize((list) => list.filter((item) => item.get('isActive')));
  const activeItems = filterActive(items);

  return { activeItems };
}

Такое вот простое решение. Теперь осталось отрефакторить всё.

Установка Node на убунту

Мы же тут все фронтендом балуемся? Где фронтенд — там и нода, а иногда приходится устанавливать её на сервера. Вот и мне пришлось снова заняться этим во время переезда с Clodo в Digital Ocean.

Самая большая сложность в том, что на официальном сайте лежат только скомпилированные бинарники и исходники, а deb-пакетов нет. Раньше я ставил ноду из бинарников, но каждый раз нужно было делать кучу лишних действий, а про обновление версий я лучшее вообще тактично промолчу.

Сейчас, в 21 веке всё стало немного лучше — есть nodesource.com, который собирает пакеты и поддерживает их. Теперь установка ноды сводится к двум (трём на совсем свежем сервере) шагам.

Скачиваем скрипт и запускаем скрипт, который пропишет нужные репозитории в систему и обновит apt:

curl -sL https://deb.nodesource.com/setup_6.x | sudo bash -

Если curl не установлен — установите его через sudo apt-get install curl

Опция -s включает «молчаливый» режим для curl, а -L просит следить за редиректами. Если обратите внимание на url, то заметите, что мы запросили установочный скрипт самой свежей версии 6-й ноды.

Скрипт будет скачан и запущен в bash. В процессе выполнения он будет информировать вас о выполняемых шагах.

Как только скрипт завершит свою работу — можно установить ноду:

sudo apt-get install nodejs

Всё, нода установлена и, что особенно важно, готова к будущим обновлениям через apt-get upgrade.

Автоматический деплой из Тревиса

Между делом я поддерживаю на плаву сервер сообщества Веб-Стандарты. Среди прочего там у нас расположен сайт проекта Web Standards Days. Сайт простой, голая статика, но собирался и деплоился вручную. Нет, мы пробовали генераторы статики, и даже писали свой — но не взлетело. И вот что-то подустал Вадим руками ещё и деплоить, решили попробовать автоматизировать деплой через Тревис (в dev.opera.com так сделано, например). Оказалось, что в этом нет ничего сложного.

Деплой у нас происходит крайне примитивно: файлы проекта прогоняются через пачку gulp-тасков и получается набор из html, css и картинок. Всё это через ещё один gulp-таск (на самом деле это просто обёртка над rsync) отправляется на сервер. Ревизий, роллбека и вот этого всего, что есть в капистрано у нас нет, да и не очень нужно — всё же в гите лежит. А вот чтобы положить всё на сервер, нужно туда залогиниться. Логин только по ssh-ключу и встаёт проблема — как положить приватный ключ в тревис?

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

Шифрование ключа

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

mkdir .travis
ssh-keygen -f .travis/deploy_key

Ключ -f создаёт файлы с нужным нам именем, без дополнительного вопроса со стороны генератора, каталог .travis просто для порядка в репозитории, и его нужно создать до вызова ssh-keygen.

Теперь самое вкусное — шифруем ключ. Для работы нужно установить CLI для Тревиса, если он ещё не установлен. Вот ссылка на руководство по установке.

travis encrypt-file .travis/deploy_key .travis/deploy_key.enc --add
  • Первый аргумент — путь к исходному файлу, второй — путь к зашифрованному файла. Если не указать, куда класть шифрованный файл, то тревис положит его в текущий каталог.
  • Ключ --add сразу добавляет нужную команду для расшифровки в .travis.yml.

В процессе шифрования тревис создаст пару секретных переменных окружения и добавит их в настройки репозитория. Чтобы всё прошло успешно, перед началом работы утилита travis спросит логин и пароль от аккаунта на travis-ci.org, а также проверит, что мы в каталоге с репозиторием, подключённым к тревису.

Файл .travis/deploy_key.enc нужно добавить в репозиторий, а вот исходный deploy_key — ни в коем случае. Лучше его вообще удалить. Публичный ключ нужно добавить в ~/.ssh/authorized_keys на сервере, в тот аккаунт, от имени которого будет выполняться деплой. В нашем случае это было так:

ssh-copy-id -i .travis/deploy_key.pub travis@web-standards.ru

Утилита ssh-copy-id для мака берётся из homebrew, на линуксах она есть сразу.

Скрипт деплоя

Как было указано выше, вызов утилиты travis с ключом --add добавил в .travis.yml команду для расшифровки ключа на билд-машине, но на мой взгляд внутри Yaml всё это выглядит очень страшно. Кроме того, нам нужно не только расшифровать ключ, но и выполнить пару дополнительных команд, поэтому создадим ещё один служебный файл в нашем каталоге — .travis/set-up-ssh. Файлик нужно сразу сделать исполняемым:

chmod a+x .travis/set-up-ssh

Чтобы получить возможность подключиться по ssh с билд-машины, нужно чтобы наш ключ был опознан как ssh-ключ, для этого нужно поменять права доступа к файлу ключа и переименовать его. Если нужны дополнительные параметры ssh-соединения — укажем их в файле ~/.ssh/config. В итоге у нас получился вот такой файл настройки ssh для деплоя.

#!/usr/bin/env bash
openssl aes-256-cbc -K $encrypted_123456789dfs_key -iv $encrypted_123456789dfs_iv -in ./.travis/deploy_key.enc -out ${TRAVIS_BUILD_DIR}/.travis/deploy_key -d
chmod 600 ${TRAVIS_BUILD_DIR}/.travis/deploy_key
mv ${TRAVIS_BUILD_DIR}/.travis/deploy_key ~/.ssh/id_rsa
cat ${TRAVIS_BUILD_DIR}/.travis/ssh_config >> ~/.ssh/config
  1. shebang, куда без него;
  2. расшифровка ключа. Для того, чтобы не было проблем с путями к файлам, используем переменную $TRAVIS_BUILD_DIR, подробнее про доступные в тревисе переменные на можно прочитать в документации;
  3. Меняем права у файла, иначе ssh откажется его читать;
  4. Кладём ключ под стандарным именем, можно указать путь в ~/.ssh/config, но как-то лень;
  5. Дописываем нужные нам параметры в ~/.ssh/config, например, имя пользователя, под которым будем логиниться на сервер.

Рассказываем тревису, как деплоить

Теперь, когда у нас есть ключ и скрипт настройки ssh, можно дописать в .travis.yml правила, по которым будет деплоиться. С недавнего времени в тревисе появилась отдельная стадия, которая так и называется deploy. Эта стадия выполянется только после успешного прохождения тестов.

Для деплоя можно выбрать предустановленные настройки для разных облачных провайдеров или использовать собственный скрипт. В нашем случае в .travis.yml был добавлен такой фрагмент:

before_deploy:
    - .travis/set-up-ssh
deploy:
  skip_cleanup: true
  provider: script
  script: npm run deploy
  on:
    branch: master

Также, как и для npm-скриптов, для этапов жизненного цикла процесса в тревисе есть pre- и post-хуки. Мне они прямо вот очень нравятся, сразу становится понятно, что тут важно, а у чего более утилитарная роль. У нас перед деплоем выполняется настройка ssh.

В секции деплоя мы говорим тревису не очищать каталог после прогона тестов (там как раз вся наша сборка, зачем ещё раз всё собирать?) и указываем, что нужно выполнить нашу кастомную команду (provider: script), команда для деплоя максимально простая — npm run deploy, так что в любой момент мы можем соскочить с галпа на что-то другое. Хоть обратно на make-файлы. Ну и естественно, авто-деплоимся мы только из ветки мастер.

Итого

Теперь, как только вы увидите опечатку на нашем сайте — вы можете прислать нам пулл-риквест, и в течение пары минут после его принятия фикс будет в продакшне. Обратная сторона простоты процесса — деплой будет происходить при любом пуше в master, даже если в коммите меняется что-то, не имеющее отношения к сайту — например, Readme.md или .gitignore, но тут нам помогает rsync, который отправить на сервер только то, что действительно должно быть на сервере.