今年六月发布的 Elixir 1.9 已经自带 mix release 功能,所以这一篇里我将尝试把项目从 distillery 迁移至 mix release。
首先,移除 mix.exs
中的 distillery:
- {:distillery, "~> 2.0"},
接着执行 mix deps.clean distillery --unlock
将 distillery 从 mix.lock
中移除。
之后删掉 distillery 配置文件目录 rel
。
我们知道,Phoenix 的配置分两种:
-
构建时配置 - 在构建发行包时读取
-
运行时配置 - 发行包部署至生产环境启动时读取
distillery 通过 Config Providers 来解决运行时配置的问题,Elixir 1.9 则是新增了 config/releases.exs
来专门存放运行时配置。
我们在 config
目录下新建一个 releases.exs
文件,并将 prod.exs
中运行时配置迁移过来:
import Config
# Configures token for telegram bot
config :telegram_bot,
token: System.fetch_env!("TELEGRAM_TOKEN")
# Configures extwitter oauth
config :extwitter, :oauth,
consumer_key: System.fetch_env!("TWITTER_CONSUMER_KEY"),
consumer_secret: System.fetch_env!("TWITTER_CONSUMER_SECRET")
config :tweet_bot, TweetBotWeb.Endpoint, secret_key_base: System.fetch_env!("SECRET_KEY_BASE")
# Configure your database
config :tweet_bot, TweetBot.Repo,
username: System.fetch_env!("DATABASE_USER"),
password: System.fetch_env!("DATABASE_PASS"),
database: System.fetch_env!("DATABASE_NAME"),
hostname: System.fetch_env!("DATABASE_HOST")
注意,我们这里用的是 import Config
,不是 use Mix.Config
,因为发行包里不会有 Mix
,所以 elixir 1.9 里新增了 Config
用于替换 Mix.Config
。另外我们将旧的 System.get_env
改为 System.fetch_env!
,确保应用启动时环境变量已经就绪,否则将抛出错误。
在 distillery 里,我们通过 rel/config.exs
配置发行包:
environment :prod do
set(include_erts: true)
set(include_src: false)
set(cookie: :"p=$dC[$t:@5>z^yex}K}(M[U4p{V&~X~Is(bR{4sSDr5|g@K>;]O{(zHWQU<4El0")
end
...
release :tweet_bot do
set(version: current_version(:tweet_bot))
set(
applications: [
:runtime_tools
]
)
set(
config_providers: [
{Mix.Releases.Config.Providers.Elixir, ["${RELEASE_ROOT_DIR}/etc/config.exs"]}
]
)
set(
commands: [
migrate: "rel/commands/migrate.sh",
seed: "rel/commands/seed.sh"
]
)
set(
overlays: [
{:copy, "config/prod.exs", "etc/config.exs"}
]
)
set(pre_start_hooks: "rel/hooks/pre_start")
end
Elixir 1.9 下则通过 mix.exs
文件:
releases: [
tweet_bot: [
include_executables_for: [:unix]
],
我们且尝试在开发环境中运行 MIX_ENV=prod mix release
看看:
$ MIX_ENV=prod mix release ... == Compilation error in file lib/tweet_bot/repo.ex == ** (ArgumentError) missing :adapter option on use Ecto.Repo lib/ecto/repo/supervisor.ex:67: Ecto.Repo.Supervisor.compile_config/2 lib/tweet_bot/repo.ex:2: (module) (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
报错了,实际上,我们运行 iex -S mix phx.server
也能看到类似的错误:
warning: retrieving the :adapter from config files for TweetBot.Repo is deprecated. Instead pass the adapter configuration when defining the module: defmodule TweetBot.Repo do use Ecto.Repo, otp_app: :tweet_bot, adapter: Ecto.Adapters.Postgres lib/ecto/repo/supervisor.ex:100: Ecto.Repo.Supervisor.deprecated_adapter/3 lib/ecto/repo/supervisor.ex:64: Ecto.Repo.Supervisor.compile_config/2 lib/tweet_bot/repo.ex:2: (module)
我们需要按提示将 adapter
代码添加到 lib/tweet_bot/repo.ex
中,并删掉 config/releases.exs
中相应的 adapter
部分。
再次尝试在开发环境中运行 MIX_ENV=prod mix release
:
MIX_ENV=prod mix release Compiling 21 files (.ex) Generated tweet_bot app Release tweet_bot-0.0.5 already exists. Overwrite? [Yn] * assembling tweet_bot-0.0.5 on MIX_ENV=prod * using config/releases.exs to configure the release at runtime Release created at _build/prod/rel/tweet_bot! # To start your system _build/prod/rel/tweet_bot/bin/tweet_bot start Once the release is running: # To connect to it remotely _build/prod/rel/tweet_bot/bin/tweet_bot remote # To stop it gracefully (you may also send SIGINT/SIGTERM) _build/prod/rel/tweet_bot/bin/tweet_bot stop To list all commands: _build/prod/rel/tweet_bot/bin/tweet_bot
一切顺利。
在使用 distillery 时,我曾写过一个 build.sh
用于在 docker 中执行构建过程:
#!/usr/bin/env bash
set -e
cd /opt/build/app
APP_NAME="$(grep 'app:' mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g')"
APP_VSN="$(grep 'version:' mix.exs | cut -d '"' -f2)"
mkdir -p /opt/build/rel/artifacts
export MIX_ENV=prod
# Fetch deps and compile
mix deps.get --only prod
# Run an explicit clean to remove any build artifacts from the host
mix do clean, compile --force
cd ./assets
npm install
npm run deploy
cd ..
mix phx.digest
# Build the release
mix release
# Copy tarball to output
cp "_build/prod/rel/$APP_NAME/releases/$APP_VSN/$APP_NAME.tar.gz" rel/artifacts/"$APP_NAME-$APP_VSN.tar.gz"
exit 0
我们需要做些调整:
#!/usr/bin/env bash
set -e
cd /opt/build/app
APP_NAME="$(grep 'app:' mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g')"
APP_VSN="$(grep 'version:' mix.exs | cut -d '"' -f2)"
export MIX_ENV=prod
# Fetch deps and compile
mix deps.get --only prod
# Run an explicit clean to remove any build artifacts from the host
mix do clean, compile --force
cd ./assets
npm install
npm run deploy
cd ..
mix phx.digest
# Build the release
mix release
# Copy tarball to output
# cp "_build/prod/rel/$APP_NAME/releases/$APP_VSN/$APP_NAME.tar.gz" rel/artifacts/"$APP_NAME-$APP_VSN.tar.gz"
exit 0
我们去掉了 --env=prod
,并注释掉了 tarball
相关的代码,因为 mix release
不会像 distillery 一样生成 .tar.gz 文件,需要我们自行压缩。
我们仍要用 docker 来构建,只不过这回 dockerfile 也需要更新到 elixir 1.9 了。
接下来在命令行下执行:
$ docker run -v $(pwd):/opt/build/app --rm -it chenxsan/elixir-1.9-ubuntu-16.04:latest /opt/bui
ld/app/bin/build.sh
就可以在项目根目录下的 _build/prod/rel/tweet_bot
得到我们的发行包 - 可在 Ubuntu 16.04 上运行的发行包。将目录打包成 tweet_bot.tar.gz 上传至生产环境解压即可部署。
在启动程序前,我们需要在生产环境上配置好所有环境变量。最简单的办法是 export
,比如:
$ export TELEGRAM_TOKEN=xxxxx
当然,这个方案并不可持续,因为我们每次部署都得连上服务器重新 export
一遍,没几人吃得消这样。
mix release 提供了另一个办法, rel/env.sh.eex。
不过我们不需要手动生成该文件,可以执行 mix release.init
来自动生成,之后将所有的 export
加入 rel/env.sh.eex
文件中:
export PORT=
export TELEGRAM_TOKEN
...
构建时该文件会被拷入发行包,并在程序启动前执行。
那么,我们在 mix release 下要如何 migrate 我们的数据库呢?与 distillery 类似,我们要定义一个模块,在其中执行 migrate
。我们可以复用此前的 lib/release_tasks.ex
文件,改造 如下:
defmodule TweetBot.Release do
@app :tweet_bot
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end
不过 Ecto.Migrator.with_repo
是 ecto_sql 3.1.2 新增的,而我们目前 mix.lock
中相应版本还是 3.0.3,所以需要升级一下:
$ mix deps.update ecto_sql
这样我们就可以通过 bin/tweet_bot eval "TweetBot.Release.migrate()"
来执行 migrate 了。