自托管

在本指南中,我们将学习如何在自己的软件包存储库上提供 Hex 软件包。 首先,我们先了解一下 Hex 存储库到底是什么。

Hex 规范

Hex 部署必须遵循 Hex 规范https://hex.org.cnhttps://github.com/hexpm/hexpm 和相关服务(例如 https://github.com/hexpm/hexdocs)提供支持。 虽然您可以使用这些项目来运行自己的 Hex 基础设施,但通常不建议这样做,因为它们包含许多平均部署不需要的特性和复杂性。 Hex 团队还维护着一个更低级的库 https://github.com/hexpm/hex_core,您可以使用它来构建和交互 Hex 服务。

规范描述了两个 端点

  1. HTTP API - 用于发布软件包、软件包搜索和管理任务。
  2. 存储库 - 用于提供注册表资源和软件包 tarball 的只读端点。

如果您只想提供软件包,则只需要实现 Repository 端点。

构建注册表

Hex v0.21 引入了 mix hex.registry build 任务,它提供了一种构建本地注册表的简单方法。

mix hex.registry build 需要三件事

  • 注册表名称
  • 用于保存公共文件的目录
  • 用于签署注册表的私钥。

让我们创建一个“acme”注册表,生成一个随机私钥,一个 public 目录,最后构建注册表

$ mkdir acme
$ cd acme
$ openssl genrsa -out private_key.pem
$ mkdir public
$ mix hex.registry build public --name=acme --private-key=private_key.pem
* creating public/public_key
* creating public/tarballs
* creating public/names
* creating public/versions

就这样!现在,我们只需要启动一个公开 public 目录的 HTTP 服务器,就可以将 Hex 客户端指向它。 但是,让我们先向我们的存储库添加一个软件包。

要发布软件包,您需要将 tarball 复制到 public/tarballs 并重新构建注册表。 您可以构建自己的软件包(使用 mix hex.build)或简单地使用现有软件包。 让我们使用后者

$ mix hex.package fetch decimal 2.0.0
decimal v2.0.0 downloaded to decimal-2.0.0.tar
$ cp decimal-2.0.0.tar public/tarballs/
$ mix hex.registry build public --name=acme --private-key=private_key.pem
* creating public/packages/decimal
* updating public/names
* updating public/versions

现在让我们测试一下我们的存储库。 我们可以使用 Erlang/OTP 附带的内置服务器来提供 public 目录

$ erl -s inets -eval 'inets:start(httpd,[{port,8000},{server_name,"localhost"},{server_root,"."},{document_root,"public"}]).'

现在,让我们添加存储库并尝试获取我们刚刚发布的软件包

$ mix hex.repo add acme http://localhost:8000 --public-key=public/public_key
$ mix hex.package fetch decimal 2.0.0 --repo=acme
decimal v2.0.0 downloaded to decimal-2.0.0.tar

如果一切顺利,您应该看到软件包是从您的本地服务器下载的!

要在您的 Mix 项目中使用该软件包,请将其添加为依赖项并将 :repo 选项设置为您的存储库名称

defp deps() do
  {:decimal, "~> 2.0", repo: "acme"}
end

在接下来的部分中,我们将介绍如何将注册表部署到生产环境。

部署到 S3

部署到 Amazon S3(或类似的云服务)可能是拥有可靠 Hex 存储库的最简单方法。

如果您已经有一个 S3 存储桶,请使用例如 AWS CLI 来同步 public/ 目录的内容,如下所示

$ aws s3 sync public s3://my-bucket

警告: 请记住仅同步公共目录,而不是 private_key.pem! 而且,如果您确实要同步私钥,请记住设置适当的存储桶策略,以防止意外公开。

您的存储库现在应该可以在以下 URL 下使用:https://<bucket>.s3.<region>.amazonaws.com 或您配置存储桶的方式。

如果您还没有存储桶,请创建一个! 默认情况下,存储在 S3 上的文件不可公开访问。 您可以通过在存储桶属性中设置以下存储桶策略来启用公开访问

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

您也可以考虑为访问存储桶的用户添加 IAM 策略

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ]
    }
  ]
}

有关更多信息,请参阅 Amazon S3 文档,并记住以适合您的部署方式自定义存储桶/IAM 策略。

使用 Plug.Cowboy & Docker 部署

如果您需要对 Hex 服务器进行任何自定义,可以考虑创建一个合适的 Elixir 项目。 让我们这样做:我们将提供静态文件,添加基本身份验证,通过环境变量进行配置,并为使用 Docker 进行部署做好准备。

让我们启动一个新项目

$ mix new my_app --sup
$ cd my_app

并添加依赖项

# mix.exs

defp deps do
  [
    {:plug, "~> 1.11"},
    {:plug_cowboy, "~> 2.4"}
  ]
end

并更新我们的监督树以启动 Cowboy

# lib/my_app/application.ex

@impl true
def start(_type, _args) do
  port = Application.fetch_env!(:my_app, :port)

  children = [
    {Plug.Cowboy, scheme: :http, plug: MyApp.Plug, options: [port: port]}
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

最后,让我们实现 MyApp.Plug

# lib/my_app/plug.ex

defmodule MyApp.Plug do
  use Plug.Builder

  plug(Plug.Logger)
  plug(:auth)
  plug(:static)
  plug(:not_found)

  defp auth(conn, _opts) do
    auth = Application.fetch_env!(:my_app, :auth)
    Plug.BasicAuth.basic_auth(conn, auth)
  end

  defp static(conn, _opts) do
    public_dir = Application.fetch_env!(:my_app, :public_dir)
    opts = Plug.Static.init(at: "/", from: public_dir)
    Plug.Static.call(conn, opts)
  end

  defp not_found(conn, _opts) do
    send_resp(conn, 404, "not found")
  end
end

我们已准备好为发布准备应用程序! 让我们从定义运行时配置开始

# config/runtime.exs

import Config

config :my_app,
  port: String.to_integer(System.get_env("PORT", "8000")),
  auth: [
    username: System.get_env("AUTH_USERNAME", "hello"),
    password: System.get_env("AUTH_PASSWORD", "secret")
  ],
  public_dir: System.get_env("PUBLIC_DIR", "tmp/public")

我们允许应用程序使用环境变量进行配置,但为了方便起见,我们还提供默认值。 我们已准备好组装发布!

$ MIX_ENV=prod mix release

让我们运行它! 我们将提供我们在指南第一部分中创建的本地存储库的 public 目录

$ PORT=8000 PUBLIC_DIR=$HOME/acme/public _build/prod/rel/my_app/bin/my_app start

由于我们添加了基本身份验证,让我们更新存储库 URL

$ mix hex.repo set acme --url http://hello:secret@localhost:8000

让我们确保一切正常,方法是再次尝试从本地存储库获取软件包

$ mix hex.package fetch decimal 2.0.0 --repo=acme

我们已准备好将应用程序放入 Docker 容器中,让我们定义 Dockerfile

FROM hexpm/elixir:1.11.2-erlang-23.1.2-alpine-3.12.1 as build
RUN apk add --no-cache git
WORKDIR /app
RUN mix local.hex --force && mix local.rebar --force
ENV MIX_ENV=prod
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
RUN mix deps.compile
COPY lib lib
RUN mix compile
COPY config/runtime.exs config/
RUN mix release

# Start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM alpine:3.12.1 AS app
RUN apk add --no-cache openssl ncurses-libs
WORKDIR /app
RUN chown nobody:nobody /app
USER nobody:nobody
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/my_app ./
ENV HOME=/app
ENTRYPOINT ["bin/my_app"]
CMD ["start"]

让我们构建容器并运行它

$ docker build . -t my_app
$ docker run --env PUBLIC_DIR=/public --env PORT=8000 -v $HOME/acme/public:/public -p 8000:8000 my_app

请注意,我们如何使用适当的环境变量、共享卷和发布端口来配置容器。

让我们再次从本地存储库获取软件包,以测试一切正常

$ mix hex.package fetch decimal 2.0.0 --repo=acme

我们跳过了很多细节,因此如果您想了解更多信息,请务必查看