使用Release部署Phoenix应用程序

102 阅读4分钟

使用Release部署Phoenix应用程序

在大多数情况下,使用Release部署Phoenix应用程序是非常简单的。然而,对于刚刚起步的人来说,某些配置步骤可能有点令人困惑。我将尝试解决我在早期开发和部署Phoenix应用程序时遇到的一些问题。

这篇文章假设你至少已经有了一个基本的Phoenix应用程序,它连接到PostgreSQL数据库,并且已经准备好被部署。在这篇文章中,我把我的应用程序称为Salmon

编译时与运行时的配置 - 系统环境变量

基于环境变量的配置是任何12Factor应用程序的组成部分。除此之外,当我们在CI服务器上编译我们的应用程序时,并非所有的环境变量都是可用的。其中一些环境变量值会根据生产和暂存环境而改变。

elixirconfig/*.exs 文件都是编译时的配置。如果我们在其中一个文件中添加类似这样的东西,我们将得到一个错误,而不是成功发布:

# config/prod.exs
use Mix.Config
database_url = System.get_env("DATABASE_ECTO_URL") ||
    raise """
    environment variable DATABASE_ECTO_URL is missing.
    """

config :salmon, Salmon.Repo,
  url: database_url,
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

如果我们现在运行一个mix release ,我们会得到一个运行时错误:

$ MIX_ENV=prod mix release
** (RuntimeError) environment variable DATABASE_ECTO_URL is missing.

虽然这是我们设置的一个预期结果,但我们希望当我们在生产/暂存环境中启动服务器时发生这种情况。我们可能不希望在组装发行版时设置DATABASE_ECTO_URL 的值。

只需将这些行从config/prod.exs 移到config/releases.exs ,Release就可以将应用程序配置为使用运行时配置:

$ MIX_ENV=prod mix release
* assembling salmon-1.0.0 on MIX_ENV=prod
* using config/releases.exs to configure the release at runtime

Release created at _build/prod/rel/salmon!

编译时与运行时的配置 - Elixir模块属性

编译时配置和运行时配置可能混淆的另一种情况是在使用Application.get_env/3 和Elixir模块attrs。Elixir中的模块属性是在编译时配置的。当使用模块属性来存储在运行时通过环境变量配置的值时,我们需要小心。这些值将不会反映在你的应用程序中
换句话说,Elixir中的模块属性应该只被用来存储在编译时可用的常量。其他所有在运行时发生的事情都应该使用函数。这包括Application.get_env/3 应用程序的环境查询:

# Don't
defmodule Salmon
  @base_url "https://world-fishes.com/api/v1"
  @api_access_token Application.get_env(:salmon, :api_access_token)

  def fetch_fishes() do
    ....
    headers = [
      {"token", @api_access_token},
      {"content-type", "application/json"}
    ]
    ....
  end
end

尽管当我们的OTP应用程序启动时,我们可能会根据系统的env变量值来设置salmon: :api_access_token ,但在测试和图像构建等环境中编译应用程序时,这仍然会将@api_access_token 的值设置为nil

相反,我们应该使用函数在运行时获取应用程序的配置值,因为:

# Do
defmodule Salmon
  @base_url "https://world-fishes.com/api/v1"

  def fetch_fishes() do
    ....
    headers = [
      {"token", api_access_token()},
      {"content-type", "application/json"}
    ]
    ....
  end

  defp api_access_token, do: Application.get_env(:salmon, :api_access_token)
end

用Release工件整合数据库和运行迁移

当我们开发一个具有数据库依赖性的phoenix应用程序时,我们经常会遇到以下mix 命令:

$ mix ecto.create
$ mix ecto.migrate

这些命令只不过是用来按照我们定义的模式创建和迁移我们的数据库表的两个命令。然而,当我们使用Releases ,将相同的应用程序部署到生产环境中时,我们可能会遇到一个路障。

在使用docker维护用于部署的镜像时,我们经常从alpine ,以保持镜像的最小尺寸。在此基础上,Elixir与容器化工作顺利。这有助于我们通过使用_build 目录中的发布工件来构建薄的docker镜像。然而,Mix 构建工具在我们的发布工件中是不可用的。

作为部署的一部分,运行迁移是至关重要的,幸运的是我们有一个整洁的小工作方法来解决这个问题。我们的发布版二进制文件支持bin/salmon eval <expression> 命令,可以用来运行Elixir表达式。我们所要做的就是在我们的Salmon 应用程序中创建一个模块,它可以在Ecto 的帮助下运行迁移:

# lib/salmon/release.ex
defmodule Salmon.Release do
  @app :salmon

  def migrate do
    load_app()
    maybe_create_db()

    for repo <- all_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 all_repos do
    Application.fetch_env!(@app, :ecto_all_repos)
  end

  defp load_app do
    Application.load(@app)
  end

  defp maybe_create_db() do
    for repo <- all_repos() do
      :ok = ensure_repo_created(repo)
    end
  end

  defp ensure_repo_created(repo) do
    IO.puts("==> Create #{inspect(repo)} database if it doesn't exist")

    case repo.__adapter__.storage_up(repo.config) do
      :ok ->
        IO.puts("*** Database created! ***")
        :ok

      {:error, :already_up} ->
        IO.puts("==> Database already exist <(^_^)>")
        :ok

      {:error, term} ->
        {:error, term}
    end
  end
end

现在我们可以建立docker镜像,然后通过运行以下命令来部署,作为docker run 的一部分:

$ _build/prod/rel/salmon/bin/salmon eval "Salmon.Release.migrate"

$ _build/prod/rel/salmon/bin/salmon start

如果你要将其部署到Kubernetes集群,你可以使用Jobs来运行eval migrate命令。

Phoenix官方文档在处理上述使用Release发布Phoenix/Elixir应用程序的细微差别方面对我帮助很大。

希望你觉得这篇文章对你有帮助。如果你有兴趣了解如何在Kubernetes集群中部署类似的应用,请继续关注我们的博客。我将在未来的文章中写到这一点!