Distillery (基础)

This translation is up to date.

Distillery 是纯 Elixir 编写的发布管理工具。它可以让你在极少,甚至不需要配置的情况下生成发布包,并部署到其它环境。

什么是发布包?

一个发布包就是包含了 Erlang/Elixir 编译码的代码包(也就是 BEAM 字节码)。它里面还提供运行程序必须的脚本。

当你编写了一个或多个应用后,你或许想创造一个包含了这些应用和 Erlang/OTP 应用子集的完整系统。这个就是一个发布包。—— Erlang 文档

发布包简化了应用测部署:它们是自包含的,并提供了启动所需的所有东西;还可以通过内置命名行脚本来轻松地管理它们,包括打开远程命令行,启动/停止/重启应用,后台启动,发送远程命令等。另外,它们还是可归档保存的成品,意味着你可以在未来任意时刻,通过这个压缩包恢复到旧版本的系统(除非和底层的 OS 或系统库不兼容)。发布包的使用是热更新或降级的必备条件,而这正是 Erlang VM 最强大的特性之一。—— Distillery 文档

一个发布包包含以下内容:

  • /bin 文件夹
    • 这包含了整个应用启动的脚本。
  • /lib 文件夹
    • 这包含了应用编译后的字节码,及其所有依赖。
  • /releases 文件夹
    • 这包含了发布包,及其钩子和自定义命令的元数据。
  • /erts-VERSION
    • 这是 Erlang 运行时环境。它可以让你的机器在没有安装 Erlang 或 Elixir 的情况下运行你的应用。

入门及安装

把 Distillery 当作依赖,添加到你项目里 的 mix.exs 文件里头。注意 —— 如果你的是 umbrella 应用,请把它添加到项目根目录的 mix.exs 文件里。

defp deps do
  [{:distillery, "~> 2.0"}]
end

然后在命令行输入:

mix deps.get
mix compile

生成发布包

在命令行,运行

mix release.init

命令完成后会产生一个 rel 目录,并包含一些配置文件在里面。

然后运行 mix release 就可以生成一个发布包。

发布包一生成,命令行应该会出现以下指引。

==> Assembling release..
==> Building release book_app:0.1.0 using environment dev
==> You have set dev_mode to true, skipping archival phase
Release successfully built!
To start the release you have built, you can use one of the following tasks:

    # start a shell, like 'iex -S mix'
    > _build/dev/rel/book_app/bin/book_app console

    # start in the foreground, like 'mix run --no-halt'
    > _build/dev/rel/book_app/bin/book_app foreground

    # start in the background, must be stopped with the 'stop' command
    > _build/dev/rel/book_app/bin/book_app start

If you started a release elsewhere, and wish to connect to it:

    # connects a local shell to the running node
    > _build/dev/rel/book_app/bin/book_app remote_console

    # connects directly to the running node's console
    > _build/dev/rel/book_app/bin/book_app attach

For a complete listing of commands and their use:

    > _build/dev/rel/book_app/bin/book_app help

在命令行输入命令 ` _build/dev/rel/MYAPP/bin/MYAPP foreground` 就可以启动你的应用。当然,你需要把 MYAPP 替换为你的项目名称。这样我们就已经通过发布包来运行我们的应用了!

在 Phoenix 项目中使用 Distillery

如果你需要结合 Phoenix 来使用 Distillery,有一些额外的步骤需要执行。

首先,编辑 config/prod.exs 文件。把以下内容:

config :book_app, BookAppWeb.Endpoint,
  load_from_system_env: true,
  url: [host: "example.com", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

更改为:

config :book_app, BookApp.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "localhost", port: {:system, "PORT"}],
  cache_static_manifest: "priv/static/cache_manifest.json",
  server: true,
  root: ".",
  version: Application.spec(:book_app, :vsn)

这里做的修改是:

  • server —— 在系统启动的时候,运行 Cowboy 应用的 http 服务
  • root —— 配置系统根目录,也就是放置并提供静态文件的路径。
  • version —— 当系统版本升级的时候,系统缓存就会被清除。
  • port —— 根据 ENV 环境变量,在系统启动的时候设置端口,通过 PORT=4001 _build/prod/rel/book_app/bin/book_app foreground

如果执行上述命令的时候,系统由于找不到数据库而崩溃了。我们可以通过 Ecto mix 命令来修复这个错误。在命令行,输入:

MIX_ENV=prod mix ecto.create

这个命令可以帮你创建数据库。尝试重新启动系统,这时候应该正常了。但是,你会发现数据库的升级脚本还没有运行。通常,在开发阶段,这些升级脚本都是手动调用 mix.ecto migrate 来运行的。到了发布阶段,我们希望它能自动按照配置运行。

在生产环境运行数据库升级脚本

Distillery 可以让我们在发布生命周期的不同时刻执行代码。这些点被称之为 boot-hooks。Distillery 提供的钩子包括:

  • pre_start
  • post_start
  • pre/post_configure
  • pre/post_stop
  • pre/post_upgrade

根据我们的西药,post_start 是在生产环境运行数据库升级脚本的点。我们先创建一个叫 migrate 的发布任务。这个任务是一个可以在命令行调用的模块函数入口,它包含了和系统应用去分开的代码。通常我们会把那些系统本身不需要运行的任务都放在这里。

defmodule BookAppWeb.ReleaseTasks do
  def migrate do
    {:ok, _} = Application.ensure_all_started(:book_app)

    path = Application.app_dir(:book_app, "priv/repo/migrations")

    Ecto.Migrator.run(BookApp.Repo, path, :up, all: true)
  end
end

注意 通常,好的做法是确保系统各部分都正常启动后,再运行这些升级脚本。Ecto.Migrator 可以帮助我们连接数据库,然后运行脚本。

接着,创建新的文件 —— rel/hooks/post_start/migrate.sh 并加入如下代码:

echo "Running migrations"

bin/book_app rpc "Elixir.BookApp.ReleaseTasks.migrate"

我们需要使用 Erlang 的 rpc 模块,通过远程程序调用服务(Remote Produce Call)来正确执行代码。简单来说,它能让我们在远程节点执行函数调用,获取回结果。一般来说,我们的系统应用在生产环境都是运行在好几个不同的节点上面的。

最后,在 rel/config.exs 文件,我们加入如下钩子到 prod 的配置里。

把一下配置:

environment :prod do
  set include_erts: true
  set include_src: false
  set cookie: :"TkJuF,3nc4)OWPBpPxPDb6mz$>)>a>/v/,l2}W*sUFaz<)bG,v*3pPESE,`XOk{,"
  set vm_args: "rel/vm.args"
end

替换为:

environment :prod do
  set include_erts: true
  set include_src: false
  set cookie: :"TkJuF,3nc4)OWPBpPxPDb6mz$>)>a>/v/,l2}W*sUFaz<)bG,v*3pPESE,`XOk{,"
  set vm_args: "rel/vm.args"
  set post_start_hooks: "rel/hooks/post_start"
end

注意 —— 这个钩子只在应用的 production 发布包里。如果我们使用默认的 development 发布包,它并不会运行。

自定义命令

当发布的时候,你可能没有权限运行 mix 命令因为在要部署的机器上并没有安装 mix。我们可以通过创建自定义命令来解决这个问题。

自定义命令是启动脚本的扩展。它和 foreground 或者 remote_console 的使用方法是一样的,也就是说,它们会成为启动脚本的一部分。就像钩子一样,它们能访问启动脚本的辅助函数和环境 —— Distillery 文档

命令和发布任务一样是函数,但不同的地方在于它们是通过命令行执行,而不是通过发布脚本来运行。

既然我们能运行升级脚本了,我们或许还需要通过命令来为数据库提供基础数据。首先,在我们的发布任务模块添加一个新的函数。在 BookAppWeb.ReleaseTasks,加入以下代码:

def seed do
  seed_path = Application.app_dir(:book_app_web, "priv/repo/seeds.exs")
  Code.eval_file(seed_path)
end

接着,创建文件 rel/commands/seed.sh 并加入代码:

#!/bin/sh

release_ctl eval "BookAppWeb.ReleaseTasks.seed/0"

注意 - release_ctl() 是 Distillery 提供的脚本,可以让我们在本地或者一个干净的节点运行命令。如果需要在一个运行中的节点执行命令,你需要使用 release_remote_ctl()

需要了解更多 Distillery 的脚本,可以参考这里

最后,在 rel/config.exs 文件里,加入:

release :book_app do
  ...
  set commands: [
    seed: "rel/commands/seed.sh"
  ]
end

谨记,需要运行 MIX_ENV=prod mix release 来重新生成发布包。一旦完成,你就可以在命令行运行 PORT=4001 _build/prod/rel/book_app/bin/book_app seed

Caught a mistake or want to contribute to the lesson? Edit this page on GitHub!