Skip to content

Latest commit

 

History

History
330 lines (244 loc) · 9.35 KB

migrate-distillery-to-mix-release.asc

File metadata and controls

330 lines (244 loc) · 9.35 KB

distillery 迁移至 mix release

今年六月发布的 Elixir 1.9 已经自带 mix release 功能,所以这一篇里我将尝试把项目从 distillery 迁移至 mix release。

移除 distillery

首先,移除 mix.exs 中的 distillery:

- {:distillery, "~> 2.0"},

接着执行 mix deps.clean distillery --unlock 将 distillery 从 mix.lock 中移除。

之后删掉 distillery 配置文件目录 rel

config/releases.exs

我们知道,Phoenix 的配置分两种:

  1. 构建时配置 - 在构建发行包时读取

  2. 运行时配置 - 发行包部署至生产环境启动时读取

distillery 通过 Config Providers 来解决运行时配置的问题,Elixir 1.9 则是新增了 config/releases.exs 来专门存放运行时配置。

我们在 config 目录下新建一个 releases.exs 文件,并将 prod.exs 中运行时配置迁移过来:

config/releases.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!,确保应用启动时环境变量已经就绪,否则将抛出错误。

配置 release

在 distillery 里,我们通过 rel/config.exs 配置发行包:

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 文件:

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

一切顺利。

build.sh

在使用 distillery 时,我曾写过一个 build.sh 用于在 docker 中执行构建过程:

bin/build.sh
#!/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 文件中:

rel/env.sh.eex
export PORT=
export TELEGRAM_TOKEN
...

构建时该文件会被拷入发行包,并在程序启动前执行。

migrate

那么,我们在 mix release 下要如何 migrate 我们的数据库呢?与 distillery 类似,我们要定义一个模块,在其中执行 migrate。我们可以复用此前的 lib/release_tasks.ex 文件,改造 如下

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_repoecto_sql 3.1.2 新增的,而我们目前 mix.lock 中相应版本还是 3.0.3,所以需要升级一下:

$ mix deps.update ecto_sql

这样我们就可以通过 bin/tweet_bot eval "TweetBot.Release.migrate()" 来执行 migrate 了。

pre_start

不,mix release 没有提供 pre_start。具体原因及可能的解决办法见 链接

启动

$ bin/tweet_bot daemon