最初发表于https://geeks.wego.com和https://darnahsan.medium.com/working-with-elixir-releases-and-performing-ci-cd-in-containers-383ba03b67682020 年 12 月 24 日.

Elixir 在Wego中得到了快速采用,在周末项目之后,多项服务已经投入生产UltronEx成为监控实时数据的日常驱动程序它激发了探索新选项和构建强大弹性系统的运动.

当我们在容器中运行几乎所有工作负载时,我们利用相同的构建管道来构建容器映像以运行 CI/CD。 Elixir 在BEAM上运行,并具有一些不错的功能,例如热重新部署,这与容器的概念相反,因为它们是不可变的图像,但这并不意味着 Elixir 对容器不友好。 Mix,类似于 Rake 用于 Ruby 的 elixir 构建工具,并且非常支持生成包含ERTS的捆绑包,称为Release,只要它与构建的平台相同,您就可以在任何地方使用和运行它上。这对于在没有任何外部依赖的情况下保持图像大小非常小的容器非常有效。尽管这使得 Elixir Release 易于使用,但它也剥离了它的工具链,您需要执行诸如运行迁移或执行任务等操作,因为 Release 中没有可用的 Mix。下图显示了不同的方法及其功能

[](https://res.cloudinary.com/practicaldev/image/fetch/s--T8-B5zpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images- 1.medium.com/max/2000/1%2AobH6IHAdpZ7f2gIwyPxgjw.png)

有人可能会问为什么要使用 Release,因为 Elixir 论坛上有一个很棒的帖子,说明为什么要Always use Releases。假设你要使用 Mix 打包你的应用程序并使用 mix run --no-halt 运行它来克服缺少 Mix 的问题,它对容器大小有什么影响? 1.11.2-erlang-23.1.3 的 elixir 映像大约为 214MB,用作在多阶段 Dockerfile 中构建版本或使用 Mix 运行应用程序的基础。与那个 Debian 映像相比只有 69.2MB。当我们为 Phoenix 应用程序使用 Elixir 版本并将其复制到基于 Debian 的阶段时,最终图像大小为 101MB,而如果我们使用 Mix 而不生成版本,最终图像大小为 627MB,是发布图像的 6 倍尺寸。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--EPdPD3-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images- 1.medium.com/max/2400/0%2AbhEOsbFr_KDeTtwe.png)

[](https://res.cloudinary.com/practicaldev/image/fetch/s--Rw3iOeab--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1。 medium.com/max/2400/0%2AQLZhL17fymeoYMaw.png)

[](https://res.cloudinary.com/practicaldev/image/fetch/s--pEma9Rjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1。 medium.com/max/2400/0%2AdQEXLDfZSieJEx7h.png)

[](https://res.cloudinary.com/practicaldev/image/fetch/s--HQ4b8I6M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1。 medium.com/max/2400/0%2AEjYkDkwOMRVJCW7N.png)

这是版本相对于其他方法的一些显着优势,并且还继续显示多阶段构建文件可能对图像大小产生的影响。

所以我们应该总是使用 Releases 但如何获得 Mix 丢失的功能?为此,Elixir 和 Docker 都尽了自己的一份力量,以无缝地使事情类似于您可以使用 rake 使用 Ruby CI/CD 容器化进程所做的事情。对于使用版本](https://hexdocs.pm/phoenix/releases.html#ecto-migrations-and-custom-commands)运行[数据库迁移,Elixir 可以使用Ecto Migrator在其中创建一个单独的文件来执行迁移,它是一个标准化代码,实际上 IMO 应该是 Ecto 的一部分

defmodule MyApp.Release do
  [@app](http://twitter.com/app) :my_app

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](http://twitter.com/app), :ecto_repos)
  end

defp load_app do
    Application.load([@app](http://twitter.com/app))
  end
end

进入全屏模式 退出全屏模式

创建发布后,您可以使用 eval 在生产环境中运行迁移,例如

$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"

进入全屏模式 退出全屏模式

迁移是一个特例,Ecto 有一些很好的特性来访问数据库,使这一切成为可能,但在生产环境中运行一些任务需要单独的方法和对 Elixir 应用程序或在 Phoenix 中构建的 Web 应用程序的一些了解。每个 Elixir 应用程序都是由多个应用程序构建的,其中 Phoenix 应用程序是一个应用程序列表,其中 Web 和数据库是独立的应用程序,这也为 Elixir 应用程序提供了稳健性,任何单个应用程序都不会破坏整个服务。为此,您可以从elixir 发布文档和对 elixir 应用程序的基础了解来制定您的应用程序容器映像,以用于执行任务并运行应用程序服务。让我们以凤凰应用 GhostRider 为例。在这里,我们将应用程序子项拆分为两个单独的列表 base 和 web 。根据需要,我们可以处理从环境变量中加载的子项,这些变量可以在运行时方便地传递给容器。

defmodule GhostRider.Application do
  # See [https://hexdocs.pm/elixir/Application.html](https://hexdocs.pm/elixir/Application.html)
  # for more information on OTP Applications
  [@moduledoc](http://twitter.com/moduledoc) false

use Application

def start(_type, _args) do
    base = [
      # Start the Ecto repository
      GhostRider.Repo,
      {Finch, name: GhostRiderFinch}
    ]

web = [
      # Start the Telemetry supervisor
      GhostRiderWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: GhostRider.PubSub},
      # Start the Endpoint (http/https)
      GhostRiderWeb.Endpoint
      # Start a worker by calling: GhostRider.Worker.start_link(arg)
      # {GhostRider.Worker, arg},
    ]

children =
      case System.get_env("APP_LEVEL") do
        "TASK" -> base
        _ -> base ++ web
      end

# See [https://hexdocs.pm/elixir/Supervisor.html](https://hexdocs.pm/elixir/Supervisor.html)
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: GhostRider.Supervisor]
    Supervisor.start_link(children, opts)
  end

# Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    GhostRiderWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

进入全屏模式 退出全屏模式

尽管这种方法可行,但应用程序仅在发布启动时启动。为了克服这个问题,我们需要使用 eval 执行绑定到执行操作的包装函数,但是为了执行该操作,我们需要使用 Application.ensure_all_started(@app) 以及控制哪个环境变量来启动我们的应用程序应加载儿童。

[为应用程序加载所有子项](https://res.cloudinary.com/practicaldev/image/fetch/s--IxuyqQ27--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// /cdn-images-1.medium.com/max/2000/1%2AW7c6WpwnyFVqAts9X-xz2A.png)为应用程序加载所有子项

[为应用程序加载了有限的子项](https://res.cloudinary.com/practicaldev/image/fetch/s--MlkvCKtl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:/ /cdn-images-1.medium.com/max/2000/1%2AwMnK2kMqznTlTtRfJQqYmA.png)为应用程序加载了有限的孩子

迁移和任务已排序并能够使用容器,CI/CD 难题的最后一部分是,当您的版本中没有 Mix 并且版本是字节码且没有可用的测试文件夹时,您如何运行测试?这就是 docker 凭借其多阶段构建而大放异彩的地方,我们利用 docker-compose 来定位一个阶段,以便能够在构建中执行测试,以确保它们在我们拥有的不同技术堆栈中保持一致。

现在,一些 docker 和 docker-compose 配方可以将所有这些结合在一起以在 CI/CD 管道中工作。我必须承认的一件事是缺乏关于容器化 Elixir/Phoenix 应用程序的资源,社区应该努力改进这一点。下面是一个用于构建 Phoenix 应用程序的文件,您可以看到有一个名为 tester 的阶段,这就是我们将使用 docker-compose 的目标

# [https://elixirforum.com/t/could-use-some-feedback-on-this-multistage-dockerfile-1st-elixir-phoenix-deployment/30862/10](https://elixirforum.com/t/could-use-some-feedback-on-this-multistage-dockerfile-1st-elixir-phoenix-deployment/30862/10)?

########################
### Dependency stage ###
########################
FROM hexpm/elixir:1.11.2-erlang-23.1.3-debian-buster-20201012 AS base

# install build dependencies
RUN apt-get -qq update && \
    apt-get -qq -y install build-essential npm git python --fix-missing --no-install-recommends

# prepare build dir
WORKDIR /app

ARG MIX_ENV
ARG RELEASE_ENV

ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

# Update timezone
ENV TZ=Asia/Singapore

# install hex + rebar
RUN mix local.hex --force && \
    mix local.rebar --force

# set build ENV
ENV MIX_ENV=${MIX_ENV}
ENV RELEASE_ENV=${RELEASE_ENV}

COPY mix.exs mix.lock ./
COPY config config

# install mix dependencies
RUN mix deps.get --only ${MIX_ENV}
RUN mix deps.compile

COPY lib ./lib
COPY priv ./priv
#########################
####### Test Stage ######
#########################

FROM base as tester
WORKDIR /app
COPY test test

########################
# Build Phoenix assets #
########################
# Using stretch for now because it includes Python
# Otherwise you get errors, could use a smaller image though
FROM node:14.15.3-stretch AS assets
WORKDIR /app

COPY --from=base /app/deps /app/deps/
COPY assets/package.json assets/package-lock.json ./assets/
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error

COPY lib ./lib
COPY priv ./priv
COPY assets/ ./assets/

RUN npm run --prefix ./assets deploy

#########################
# Create Phoenix digest #
#########################
FROM base AS digest
WORKDIR /app

# set build ENV
ENV MIX_ENV=${MIX_ENV}
ENV RELEASE_ENV=${RELEASE_ENV}

COPY --from=assets /app/priv ./priv
RUN mix phx.digest

#######################
#### Create release ###
#######################
FROM digest AS release
WORKDIR /app

ARG MIX_ENV
ARG RELEASE_ENV
ENV MIX_ENV=${MIX_ENV}
ENV RELEASE_ENV=${RELEASE_ENV}

COPY --from=digest /app/priv/static ./priv/static

RUN mix do compile, release

#################################################
# Create the actual image that will be deployed #
#################################################
FROM debian:buster-slim AS deploy

# Install stable dependencies that don't change often
RUN apt-get update && \
  apt-get install -y --no-install-recommends \
  apt-utils \
  openssl \
  curl \
  wget && \
  rm -rf /var/lib/apt/lists/*

# Set WORKDIR after setting user to nobody so it automatically gets the right permissions
# When the app starts it will need to be able to create a tmp directory in /app
WORKDIR /app

ARG MIX_ENV
ARG RELEASE_ENV

ENV MIX_ENV=${MIX_ENV}
ENV RELEASE_ENV=${RELEASE_ENV}

ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

# Update timezone
ENV TZ=Asia/Singapore

HEALTHCHECK --start-period=10s \
  --interval=15s \
  --timeout=5s \
  --retries=3 \
  CMD curl -sSf [http://localhost:8080/heartbeat](http://localhost:8080/heartbeat) || exit 1

COPY --from=release /app/_build/${MIX_ENV}/rel/ghost_rider ./

ENV HOME=/app
# Exposes port to the host machine
EXPOSE 8080

CMD ["bin/ghost_rider", "start"]

进入全屏模式 退出全屏模式

运行应用程序的标准 docker-compose 文件与此类似,它将启动 Postgres 数据库并为您启动应用程序。

version: "3.7"
services:
  ghost-rider:
    build:
      context: .
      args:
        MIX_ENV: "${MIX_ENV}"
        RELEASE_ENV: "${RELEASE_ENV}"
    image: wego/ghost-rider
    depends_on:
      - ghost-rider-db
    restart: on-failure
    ports:
      - "8080:8080"
    environment:
      MIX_ENV: "${MIX_ENV}"
      RELEASE_ENV: "${RELEASE_ENV}"
      PORT: "8080"
      SECRET_KEY_BASE: "${SECRET_KEY_BASE}"
      DB_NAME: ghost-rider
      DB_PASSWORD: ghost-rider
      DB_HOST: ghost-rider-db
      APP_LEVEL: WEB

ghost-rider-db:
    image: postgres
    restart: always
    ports:
      - 5432:5432
    environment:
      POSTGRES_PASSWORD: ghost-rider
      POSTGRES_DB: "${POSTGRES_DB}"
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - "postgresql-data:/var/lib/postgresql/data"

volumes:
  postgresql-data:

进入全屏模式 退出全屏模式

要构建和运行您的容器映像,您将执行

$ docker-compose -f docker-compose.yml up -d

进入全屏模式 退出全屏模式

运行迁移

$ docker-compose run --rm ghost-rider bin/ghost_rider eval "Release.migrate"

进入全屏模式 退出全屏模式

要运行测试,您需要创建另一个 docker-compose 文件来覆盖默认值并定义它应该执行的目标。如果你愿意,你也可以创建一个全新的文件

version: "3.7"
services:
  ghost-rider:
    build:
      target: tester
      args:
        - MIX_ENV=test
        - RELEASE_ENV=test
    image: wego/ghost-rider-test
    ports:
      - "8080:8080"
    environment:
      - MIX_ENV=test
      - RELEASE_ENV=test
      - DB_NAME=ghost-rider-test
      - DB_PASSWORD=ghost-rider
      - DB_HOST=ghost-rider-db

进入全屏模式 退出全屏模式

有了这个,您可以简单地设置您的测试数据库并运行测试

$ docker-compose -f docker-compose.yml -f docker-compose.test.yml run --rm ghost-rider mix ecto.setup

$ docker-compose -f docker-compose.yml -f docker-compose.test.yml run --rm ghost-rider mix test

进入全屏模式 退出全屏模式

所需要的只是对 Elixir 的一些了解,它可以与 Docker 工具一起使用,就像任何其他语言一样构建镜像和执行 CI/CD,没有任何摩擦。

我希望这有助于容器化 Elixir 和 Phoenix 应用程序,因为它们与 Docker 和容器一起是令人惊叹的技术,可以让 CI/CD 构建管道变得非常简单。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐