自託管

在本指南中,我們將瞭解如何自行發佈套件儲存庫中的 Hex 套件。首先,提供一些關於 Hex 儲存庫實際為何的背景知識。

Hex 規格

Hex 部署必須遵循 Hex 規格https://hex.dev.org.twhttps://github.com/hexpm/hexpm 和相關服務(例如 https://github.com/hexpm/hexdocs)提供支援。雖然您可以使用這些專案來執行自己的 Hex 架構,但通常不建議這麼做,因為它們包含許多一般部署不需要的功能和複雜度。Hex 團隊也維護一個較低層級的函式庫 https://github.com/hexpm/hex_core,您可以使用它來建構和互動 Hex 服務。

這些規格說明了兩個 端點

  1. HTTP API - 用於發布套件、搜尋套件和管理工作。
  2. 儲存庫 - 只讀端點,可提供登錄資源和套件 tarball。

如果您只想提供套件,您只需要實作儲存庫端點。

建構儲存庫

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 https://127.0.0.1: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!如果你真的想同步你的私鑰,請記得設定適當的儲存貯體政策,這樣才不會意外外洩。

你的儲存庫現在應該可以在類似這樣的網址下使用: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

由於我們已加入基本驗證,讓我們更新儲存庫網址

$ 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

我們跳過了許多詳細資料,如果你想進一步瞭解,請務必查看