Деплой Phoenix-приложений через Edeliver. Часть 2

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

Шаг 1. Создание нового Phoenix-приложения

Чтобы создать новое приложение, следуйте этой инструкции или просто запустите соответствующую команду в терминале:

mix phoenix.new edelivered_app

Теперь Phoenix-приложение должно появиться в директории edelivered_app. Следуйте указаниям команды mix phoenix.new, и убедитесь, что приложение запускается на вашей системе.

Укажите реквизиты базы данных в файле config/prod.secret.exs. Можно смело использовать ту же базу данных, что и для dev-окружения, т.к. этот файл не добавляется в систему контроля версий, и на сервере будет его другая копия. Чтобы можно было создать релиз для продакшна, локальное приложение должно корректно работать в режиме «prod».

Убедитесь, что команды корректно выполняются:

MIX_ENV=prod mix ecto.create    # create prod database if not the same as dev database.
MIX_ENV=prod mix phoenix.server # run phoenix server in prod mode.

Инициализируйте пустой Git-репозиторий и сделайте начальный коммит.

git init
git commit -am "Initial commit. First!"

Шаг 2. Настройка distillery для создания релизов

Как гласит документация distillery:

Релиз — это пакет, состоящий из файлов с расширением .beam, включающих зависимости приложения, sys.config, vm.args, сценарий загрузки, а также различные утилиты и файлы метаданных, предназначенные для управления релизом после его установки. Кроме этого, релиз может также включать копию ERTS.

Фактически релиз представляет собой архив (*.tar.gz) со скомпилированным приложением и всеми его зависимостями, включая саму среду выполнения.

Созданный релиз помещается на сервер, и затем происходит его развёртывание с помощью Edeliver (подробнее здесь).

Добавьте в mix.exs следующий код:

defp deps do
  [
    #...,
    {:distillery, "~> 1.0"}
  ]
end

и запустите

mix deps.get

Создайте файл конфигурации релиза:

mix release.init

Откройте созданный файл rel/config.exs. По умолчанию релиз будет сконфигурирован для двух окружений: dev и prod. В конфигурацию можно добавлять и другие релизы и окружения, однако пока остановимся на конфигурации по умолчанию, но с одной оговоркой.

Создавать и запускать distillery-релиз в режиме dev — бессмысленно, так как это всё равно не сработает (https://github.com/bitwalker/distillery/issues/25).

Поэтому нужно поставить окружение prod в настройках по умолчанию. Замените default_environment в rel/config.exs на «prod».

  use Mix.Releases.Config,
  # This sets the default release built by `mix release`
  default_release: :default,
  # This sets the default environment used by `mix release`
  default_environment: :prod # <------ SET THIS TO :prod

Обязательно загляните на страницу терминологии distillery. Это поможет избежать суеты при возникновении ошибок и поиска в интернете решений по их исправлению.

Измените точку входа Endpoint в файле config/prod.exs на следующую:

  config :edelivered_app, EdeliveredApp.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "localhost", port: {:system, "PORT"}],
  cache_static_manifest: "priv/static/manifest.json",
  server: true,
  root: ".",
  version: Mix.Project.config[:version]

Важно, чтобы порты, указанные в опциях http и url, совпадали. Подробнее об этих опциях читайте здесь.

Теперь всё готово для создания первого релиза.

MIX_ENV=prod mix release

Даже если установить prod-окружение по умолчанию, distillery всё равно будет работать в dev-окружении Mix. Поэтому измените MIX_ENV на «prod» и только после этого выполните команду mix release.

Вот вы и создали свой релиз для продакшна!

Шаг 3. Локальное тестирование релиза distillery

По умолчанию релизы располагаются в директории _build/<env>/rel/<app-name>, и можно запускать релиз прямо оттуда. Однако, для демонстрации переносимости релизов на Elixir, извлечём архив с релизом в отдельную папку и проведём запуск из неё.

  1. Создайте директорию для своего приложения: mkdir ~/Downloads/edelivered_app
  2. Поместите копию релиза в эту директорию: cp _build/prod/rel/edelivered_app/releases/0.0.1/edelivered_app.tar.gz ~/Downloads/edelivered_app/
  3. Откройте директорию назначения: cd ~/Downloads/edelivered_app/
  4. Извлеките файлы из архива tar -zxvf edelivered_app.tar.gz Никогда не мог запомнить эти переключатели tar, так как не особо часто извлекаю файлы типа *.tar.gz. Специально для этого я создал документ в Evernote. Подсказка: создайте простенький сценарий и назовите его untar.
  5. Запустите приложение: PORT=8080 bin/edelivered_app foreground. Убедитесь, что обязательная переменная среды PORT соответствует необходимому порту.

Воспользуемся командой «foreground», чтобы для начала запустить приложение в обычном режиме и выявить существующие на данном этапе ошибки.

Если всё прошло нормально, то можно запустить приложение в фоновом режиме (в качестве демона):

PORT=8080 bin/edelivered_app start

Используйте этот же скрипт для остановки и перезапуска приложения, а также для подключения к нему. Наиболее полезные команды:

  • bin/edelivered_app выводит список доступных команд.
  • bin/edelivered_app stop останавливает приложение.
  • bin/edelivered_app ping дает обратную связь «pong», если в приложении нет ошибок.
  • bin/edelivered_app remote_console подключается к запущенному релизу через консоль IEx. В отличие от «console» и «attach», «remote_console» не завершает запущенный релиз при выходе из консоли.

После развёртывания можно использовать те же команды на продакшн сервере/серверах.

Сделайте коммит внесённых изменений: git commit -am "Add distillery dependency"

Более подробную информацию о создании релизов можно найти в инструкции по установке на странице distillery в GitHub или вот в этом чудесном гайде. Очень рекомендуется изучить документацию distillery полностью, так как в ней содержится достаточно много информации о релизах, а также рассказывается о способах устранения ошибок, к коим мы ещё вернёмся в конце данной статьи.

Шаг 4. Подготовка сервера в облаке

Нам понадобится сервер Ubuntu 16.04 с открытым 22 SSH-портом, IP адресом и root-доступом.

Для создания сервера можно пользоваться абсолютно любым провайдером облачных вычислений. Из российских хостингов отлично себя зарекомендовал Vscale. Ценовая политика у него вполне приемлема. Всё, что нам потребуется, — это минимум 1 Гб оперативной памяти. Можно, конечно, сэкономить и рассмотреть вариант с 512 Мб, но тогда придётся увеличить область подкачки, чтобы на таком сервере можно было создавать релизы, что, к слову сказать, будет происходить крайне медленно. Посмотреть на примере, как увеличить область подкачки, можно здесь. Надеюсь, выделенное красным предупреждение в самом начале поста сможет посеять в ваши мысли зерно сомнения.

Ещё один вариант — установить Ubuntu 16.04 LTS на виртуальную машину. VirtualBox вполне подойдёт. Настройте сеть виртуальной машины: выберите NAT (Network Address Translation), чтобы стало возможным подключение к ней по SSH и HTTP/HTTPS.

Дабы упростить себе жизнь, забудем про Chef, Ansible и другие DevOps инструменты.

Выполните следующее:

  1. Создайте нового пользователя «app» в домашней директории: adduser app.
  2. Поместите свой публичный RSA-ключ в /home/app/.ssh/authorized_keys для подключения к серверу по SSH без необходимости ввода пароля. Создайте директорию .ssh, если её не существует.
  3. Установите следующие права доступа:

    • chmod 700 /home/app/.ssh
    • chmod 644 /home/app/.ssh/authorized_keys
    • chown -R app:app /home/app/.ssh
  4. Для удобства добавьте пользователя «app» в группу sudo: внесите правки в файл /etc/group и поместите app в конец строки «sudo».
  5. Подключитесь к серверу по SSH в качестве пользователя app; пароль запрашиваться не должен.
  6. Следуйте инструкции по установке Ubuntu для Elixir. Установите Erlang и Elixir.
  7. Следуйте указаниям по установке NodeJs и NPM в Ubuntu.
  8. Установите git: sudo apt-get install git
  9. Установите сервер базы данных Postgres: sudo apt-get install postgresql postgresql-contrib.
  10. Настройте роль Postgres для данного пользователя:

    • Переключитесь на linux-пользователя «postgres»: sudo su - postgres
    • Запустите консольный клиент Postgres: psql
      • CREATE ROLE app WITH superuser;
      • ALTER ROLE app WITH login;
      • ALTER ROLE app WITH createdb;
      • ALTER USER app WITH PASSWORD 'coolpass'; Надеюсь, вы не будете использовать этот пароль для своих реальных серверов :)
    • Закройте консольный клиент Postres: Ctrd+D.
    • Выполните выход пользователя postgres: exit или Ctrl+D.
  11. Создайте базу данных Postgres для своего приложения: createdb edelivered_app_prod
  12. Создайте директорию приложения и настроек: mkdir /home/app/mysite.com
  13. Создайте директорию хранения релизов: mkdir /home/app/mysite.com/edeliver_release_store
  14. Добавьте глобальную переменную окружения PORT: echo "PORT=8080" | sudo tee -a /etc/environment
  15. Добавьте ещё одну глобальную переменную окружения MIX_ENV: echo "MIX_ENV=prod" | sudo tee -a /etc/environment

Шаг 5. Развёртывание distillery-релиза в продакшн с помощью Edeliver

Edeliver — это набор полезных сценариев для создания релиза при помощи distillery и его развёртывания на некоторое количество серверов. В отличие от Rails и Capistrano, Edeliver подгружает код (используя Git) только на ОДИН сервер (билд-сервер), там же компилирует его, собирает ассеты, а затем производит развёртывание готового пакета на все серверы. Capistrano по умолчанию производит развёртывание кода и компиляцию ассетов на ВСЕХ серверах.

Добавьте edeliver в список зависимостей проекта и в список приложений:

def application, do: [
  applications: [
    ...
    # Add edeliver to the END of the list
    :edeliver
  ]
]

defp deps do
  [
    ...
    {:edeliver, "~> 1.4.0"}
  ]
end

и запустите

mix deps.get

В директории проекта создайте файл .deliver/config:

#!/usr/bin/env bash
APP="edelivered_app" # <--- THIS MUST MATCH THE NAME OF THE RELEASE IN rel/config.exs
                     #      AND THE NAME OF THE APP IN config/mix.exs!!!!!!!!!!

# Configuration of where the releases would be built.
BUILD_HOST="138.197.37.15" # change to your server's IP address
BUILD_USER="app"
BUILD_AT="/home/app/mysite.com/edeliver_builds"

# The location where built releases are going to be stored.
RELEASE_STORE=app@138.197.37.15:/home/app/mysite.com/edeliver_release_store/

# Host and use of where the app would run.
PRODUCTION_HOSTS="138.197.37.15" # same host in our case.
PRODUCTION_USER="app"

DELIVER_TO="/home/app/mysite.com"

pre_erlang_get_and_update_deps() {
 # copy it on the build host to the build directory when building
 local _secret_config_file_on_build_host="/home/app/mysite.com/prod.secret.exs"

 status "Linking '$_secret_config_file_on_build_host' to build config dir"
 __sync_remote "
   ln -sfn '$_secret_config_file_on_build_host' '$BUILD_AT/config/prod.secret.exs'
 "
}

pre_erlang_clean_compile() {
 status "Installing nodejs dependencies"
 __sync_remote "
   [ -f ~/.profile ] && source ~/.profile
   set -e
   cd '$BUILD_AT'

   APP='$APP' MIX_ENV='$TARGET_MIX_ENV' npm install
 "

 status "Building brunch assets"
 __sync_remote "
   [ -f ~/.profile ] && source ~/.profile
   set -e
   cd '$BUILD_AT'

   mkdir -p priv/static
   APP='$APP' MIX_ENV='$TARGET_MIX_ENV' npm run deploy
 "

 status "Compiling code"
 __sync_remote "
   [ -f ~/.profile ] && source ~/.profile
   set -e #
   cd '$BUILD_AT'

   APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD do deps.get, compile
 "

 status "Running phoenix.digest"
 __sync_remote "
   [ -f ~/.profile ] && source ~/.profile
   set -e #
   cd '$BUILD_AT'

   APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phoenix.digest $SILENCE
 "
}

Вышеуказанная конфигурация сгенерирует на сервере следующее дерево каталогов:

/home/app/mysite.com        # Your entire app is in one place: configuration, builds and releases.
├── edeliver_builds         # This is where edeliver builds all releases: your local repo is pushed here.
│   ├── brunch-config.js
│   ├── _build
│   ├── config
│   ├── deps
│   ├── lib
│   ├── mix.exs
│   ├── mix.lock
│   ├── node_modules
│   ├── package.json
│   ├── priv
│   ├── README.md
│   ├── rel
│   ├── test
│   └── web
├── edelivered_app          # Your app is deployed here: this is where it runs, from "bin" directory.
│   ├── bin
│   ├── erl_crash.dump
│   ├── erts-8.2
│   ├── lib
│   ├── releases
│   └── var
├── edeliver_release_store # Edeliver stores all built releases here to then distribute them to servers.
│   └── edelivered_app_0.0.1.release.tar.gz
└── prod.secret.exs        # Your app's secrets: production database connection parameters.

Подтвердите изменения и пометьте коммит «0.0.1» — версией, совпадающей с версией вашего проекта в mix.exs.

git commit -am "Add edeliver configuration"
git tag 0.0.1

Теги впоследствии понадобятся для обновления релизов в edeliver.

Подключитесь к серверу по SSH и создайте файл /home/app/mysite.com/prod.secret.exs со следующим содержимым:

use Mix.Config

config :edelivered_app, EdeliveredApp.Endpoint,
  secret_key_base: "Xt317VM159wCrVgKhatAAbJcz3/yYewpbuXEpBeUpiIEOBVrTWEW878d6vADJU2u"

config :edelivered_app, EdeliveredApp.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "app",
  password: "coolpass",
  database: "edelivered_app_prod",
  pool_size: 20

Создайте релиз, произведите развёртывание и запустите его:

  1. env MIX_ENV=prod mix edeliver build release — создаёт релиз и помещает его в директорию релизов на сервере.
  2. mix edeliver deploy release to production --version=0.0.1 производит развёртывание релиза на все серверы, но не запускает его!
  3. Попробуйте на всякий случай после первого развёртывания запустить релиз в обычном режиме:
    • Подключитесь к серверу по SSH в качестве пользователя app.
    • cd ~/mysite.com/edelivered_app
    • bin/edelivered_app foreground
    • Убедитесь, что сервер работает и откройте его в браузере: [http://138.197.37.15:8080][http://138.197.37.15:8080] (ваш IP-адрес, естественно, будет другим)
    • Выполните выход: Ctrl+C
  4. mix edeliver start production запускает приложение на сервере в виде демона!

Иногда команда для запуска релиза по какой-то неведомой мне причине не справляется со своими обязанностями. Если после выполнения mix edeliver start production сайт недоступен, подключитесь к серверу по SSH и проверьте, запустился ли процесс Erlang: ps -ef | grep erl. Если нет, то ищите решение в следующем пункте.

Шаг 6. Устранение ошибок

Последняя версия образца Phoenix-приложения с настроенными distillery и edeliver доступна по ссылке: https://github.com/alex-kovshovik/edelivered_app.

  • Не забывайте прописывать MIX_ENV=prod во всех командах развёртывания.
  • Если что-то пошло не так, удалите директорию _build и попробуйте снова.
  • Не используйте автоматическую версию AUTO_VERSION с самого начала, попробуйте сперва настроить всё вручную. Мне пришлось пройти через многое в попытках сделать этот способ развёртывания таким же простым, как у Capistrano. Не забывайте увеличивать номер версии своего приложения и создавать тег с таким же именем.
  • Значение переменной APP должно совпадать с названием релиза в rel/config.exs. В противном случае получим ошибку: «Failed to build release: :no_release».
  • Значение переменной APP должно совпадать с именем приложения, иначе оно не запустится! Я пытался изменить его на «current_release», чтобы оптимизировать дерево каталогов сервера, в результате чего столкнулся с непредвиденными ошибками.
  • Полезные переключатели mix edeliver:

    • --verbose — отображает результаты работы команд;
    • --debug — показывает все команды и их вывод.
  • Внесите поправку в файл rel/config.exs, как указано в первом комментарии здесь distillery support broke with changed output_dir in distillery 1.0.0 — Issue #182.

Шаг 7. Что теперь?

В следующих статьях будет рассмотрено:

  1. Настройка nginx в качестве обратного прокси-сервера для виртуальной машины Erlang.
  2. Создание и настройка SSL-сертификата с помощью letsencrypt.
  3. Создание и развёртывание обновлённых релизов.
  4. Быстрое создание релизов в docker-контейнерах или на локальной виртуальной машине.
  5. Создание релизов на CI-сервере.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.