Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

elixir + rustで作ったアプリをdockerで動かす

More than 1 year has passed since last update.

こんばんは、今年は受験生なのでクリスマスでも勉強していたtechnoです。というか試験が年明けにあるので大学入ってからクリスマスとか正月なんてものはなくなっています。来年からは多分この状況から抜け出せると思うのですが。

前置きはともかく、elixirでアプリを作っていたのですが、httpoisonやhttpotionが特定のケースで変な挙動を起こして使うことができなかったため、rustlerを使ってrustのreqwestを使った時のノウハウです。

プロジェクトのコードはこちらにあります。

rustlerのセットアップ

mixでelixirのプロジェクトを作ったら(今回は"rustler_docker"という名前にしました)早速mix.exsを編集していきます。

mix.exs
defmodule RustlerDocker.MixProject do
  use Mix.Project

  def project do
    [
      app: :rustler_docker, # ここのアプリ名は後で使う
      version: "0.1.0",
      elixir: "~> 1.7",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:rustler, "~> 0.18.0"} # 追加
    ]
  end
end

deps.getしてからrustライブラリを生成します。

$ mix deps.get
$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > Client
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (client) > client

この時のモジュールの名前は主にelixir側のモジュールの名前になります。
ライブラリの名前は主にrust側の名前になります。

この時点でrust側のサンプル実装は終わっているのでElixir側のコードを書いていきます。まずはmix.exsの変更

mix.exs
defmodule RustlerDocker.MixProject do
  use Mix.Project

  def project do
    [
      app: :rustler_docker,
      version: "0.1.0",
      elixir: "~> 1.7",
      compilers: [:rustler] ++ Mix.compilers, # 変更
      rustler_crates: rustler_crates(), # 追加
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:rustler, "~> 0.18.0"}
    ]
  end

  # 追加
  defp rustler_crates() do
    [
      # rustライブラリの名前
      client: [
        path: "native/client", # native/#{rustライブラリの名前}
        mode: (if Mix.env == :prod, do: :release, else: :debug)
      ]
    ]
  end
end
lib/client.ex
# mix rustler.newで入力したモジュールの名前
defmodule Client do
  # opt_appはmix.exsのproject.appの名前
  # crateはrustler.newで入力したライブラリの名前
  use Rustler, otp_app: :rustler_docker, crate: :client

  def add(_a, _b), do: exit(:nif_not_loaded)
end

addという関数は生成されたrustライブラリに定義してある関数を示しています。

この時点でiexからrustのaddを呼び出せるはずです。

iex(1)> Client.add(1, 3)
{:ok, 4}

これでrustlerが使えるようになりました。

rustでやりたいことを書く

冒頭で書いた通り、rustでhttpリクエストを行うコードを書きます。

Cargo.tomlにreqwestの依存を追加して、ソースを変更します。

#[macro_use] extern crate rustler;
#[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;
extern crate reqwest; // 追加

use rustler::{Env, Term, NifResult, Encoder};

mod atoms {
    rustler_atoms! {
        atom ok;
        atom error; // コメントイン
        //atom __true__ = "true";
        //atom __false__ = "false";
    }
}

rustler_export_nifs! {
    "Elixir.Client",
    [
        ("get_body", 1, get_body) // 変更
    ],
    None
}

// 追加
fn get_body<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult<Term<'a>> {
    let url: String = try!(args[0].decode());

    match reqwest::get(&url) {
        Ok(mut res) => {
            let body = res.text().unwrap();
            Ok((atoms::ok(), body).encode(env))
        },
        Err(err) => {
            Ok((atoms::error(), err.to_string()).encode(env))
        }
    }
}

reqwest::getを呼んで結果を返しているだけですね。(簡単のためにunwrapを使っているのであまり良くない)

rust側の実装が終わったらelixir側も変更します。

lib/client.ex
defmodule Client do
  use Rustler, otp_app: :rustler_docker, crate: :client

  def get_body(_url), do: exit(:nif_not_loaded)
end

後はこのClientモジュールを使っていろいろすれば良いですね。

dockerで動かす。

以上のプロジェクトをdockerで動かします。

elixir:alpineを使いたかったのですが、

  • reqwestが使っているencoding_rsが比較的新しいrustcを要求している
  • alpineでは新しいrustcはedgeを使わないといけない
  • elixir:alpineはedgeではない
  • alpine:edgeにelixirを入れるのは面倒そう

という問題があり、良い方法が思いつかなかったのでelixir:slimの上で動かしました。
alpine3.8にedgeのrustc, cargoを入れられれば解決しそうですが、今回は見送ります(追記: できました)。

rustライブラリのビルドに必要なものを入れるとイメージが肥大するので、最初のステージでrustライブラリのビルドだけをして、それをelixirプロジェクトに載せる方針でいきます。しかし、rustlerは実行時にrustプロジェクトのコンパイルをしようとするためそのままではうまく行きません。mix.exsに細工をして環境変数によってrustライブラリをコンパイルするか指定します。

mix.exs
defmodule RustlerDocker.MixProject do
  use Mix.Project

  def project do
    [
      app: :rustler_docker,
      version: "0.1.0",
      elixir: "~> 1.7",
      compilers: compilers(), # 変更
      rustler_crates: rustler_crates(),
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:rustler, "~> 0.18.0"}
    ]
  end

  # 追加
  defp compilers do
    if System.get_env("NO_RUSTLER_COMPILE") do
      Mix.compilers
    else
      [:rustler] ++ Mix.compilers
    end
  end

  defp rustler_crates() do
    [
      client: [
        path: "native/client",
        mode: (if Mix.env == :prod, do: :release, else: :debug)
      ]
    ]
  end
end
FROM elixir:1.7.4-slim

RUN apt update && apt upgrade -y
RUN apt install -y --no-install-recommends curl build-essential libssl-dev pkg-config ca-certificates
RUN apt clean
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --no-modify-path

ENV PATH=/root/.cargo/bin/:$PATH

WORKDIR /work

COPY native/client .
RUN cargo rustc --release


FROM elixir:1.7.4-slim

RUN apt update && apt upgrade -y
RUN apt install -y ca-certificates libssl-dev

WORKDIR /app

ENV MIX_ENV=prod
ENV NO_RUSTLER_COMPILE=true

COPY mix.exs .
COPY mix.lock .
RUN mix local.hex --force
RUN mix local.rebar --force
RUN mix deps.get
RUN mix deps.compile

COPY . .
RUN mix compile

COPY --from=0 /work/target/release/libclient.so priv/native/libclient.so

イメージのサイズを削減するために最初のステージでrustコンパイラの導入とrustライブラリ(libclient.so)をビルドをして、それをelixirプロジェクトに載せています。記事を書いていて思ったのですが、Cargo.toml, Cargo.lockは先に追加して依存の解決だけしておいた方が良さそう。

/.git
/_build
/deps
/native/client/target
/priv/native

あとはできたイメージを実行してiexを起動し、Client.get_body("https://google.co.jp")とでも打ってみると動くと思います。

追記

elixir:alpineにalpine:edgeのcargo, rustを入れることができたので詳しくはDockerfileを見てください。この場合nightlyが使えないのでnightlyを使いたい場合は結局rustupを入れる必要がありそうです。

できたイメージの大きさは132MBでした。elixir:alpineのイメージサイズが86.9MBなので結構大きくなってしまいましたが、まぁこんなものでしょう。

techno-tanoC
Elixir, Haskell, Ruby, Rustが好き
mixi
全ての人に心地よいつながりを
http://mixi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away