RustのLinux muslターゲット (その2:極小Dockerイメージを実現)

Posted on
Linux-musl Docker 実践Rust入門

これは Rustその3 Advent Calendar 2019 — Qiita の8日目の記事です。

2回シリーズの2つめの記事になります。

SQLite 3と静的リンクさせる

今回はRustのアプリケーションをSQLite 3ライブラリと静的リンクさせます。 SQLiteはCで書かれた軽量コンパクトなリレーショナルデータベース管理システム(RDBMS)で、主に小規模システムやデスクトップアプリのデータストアとして利用されています。 SQLiteはそれ単独でデータベースサーバとして実行することもできますが、今回はライブラリとしてRustアプリ内に埋め込んで使います。

まずは適当なディレクトリに移動して、パッケージ(binクレート)を作成します。

$ cargo new hello-sqlite && cd $_

RustからSQLiteを使うなら rusqliteクレート が便利です。 もちろんORマッパー+クエリビルダーの Dieselクレート を使ってもかまいませんが、今回はシンプルなrusqliteを使います。

Cargo.tomldependenciesセクションにrusqliteを追加します。

hello-sqlite/Cargo.toml

[package]
name = "hello-sqlite"
edition = "2018"

[dependencies]
rusqlite = { version = "0.20.0", features = ["bundled"] }

main関数を作成します。 ほとんど意味がありませんが、SQLiteのテーブルに1から10までの整数を格納して、SQLのSUM関数でその合計を得ます。

hello-sqlite/src/main.rs

use rusqlite::{Connection, NO_PARAMS};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // メモリ上にテーブルを作成する
    // `?`演算子を使うと処理に失敗した時にmain関数から抜けてエラーを返せる
    let conn = Connection::open_in_memory()?;
    conn.execute_batch("CREATE TABLE foo(x INTEGER)")?;

    // テーブルに1から10の数字を格納する
    let mut insert_stmt = conn.prepare("INSERT INTO foo(x) VALUES(?)")?;
    for i in 1..=10 {
        insert_stmt.execute(&[i])?;
    }

    // SQLのSUM関数でその合計を得る
    let sum = conn.query_row::<i64, _, _>(
        "SELECT SUM(x) FROM foo",
        NO_PARAMS,
        |r| r.get(0)
    )?;
    println!("{}", sum);

    Ok(())
}

rust-musl-builderで簡単ビルド

hello-sqliteをmuslターゲット向けにビルドしましょう。 前回説明したようにSQLiteのような外部ライブラリを使うときは、それなりの準備が必要です。 なぜなら、外部ライブラリはglibcと動的リンクさせる代わりに、muslと静的リンクさせる必要があるからです。

とはいえ今回は自分で準備する必要はありません。 よく使われるライブラリとRustコンパイラ/gccをセットにしたDockerイメージがいくつかありますので、それらを使うのが簡単です。

今回は ekidd/rust-musl-builder(Rustマッスルビルダー)を使います。 以下のツールとライブラリが含まれています。

  • musl libcライブラリ
  • musl対応のgccコンパイラ
  • Rustのmuslターゲット
  • OpenSSLライブラリ — 多くのRustアプリケーションで必要
  • libpqライブラリ — PostgreSQLのクライアントライブラリ。Dieselなどで必要
  • libzライブラリ — zip, bzip, png画像などで使われている圧縮アルゴリズムのライブラリ。libpqで必要
  • SQLite 3 ライブラリ

このイメージの他にもgolddranksさん作成の registry.gitlab.com/rust_musl_docker/image もあります。 OpenSSLとPostgreSQLのバージョンがいくつかの組み合わせから選べるのが特徴でしょうか。 golddranksさんはSlackの日本語Rustコミュニティ(登録URL)などにもいらっしゃいますので、日本語で質問できそうなところもいいかもしれませんね。

ekidd/rust-musl-builderでビルドするには、Cargo.tomlがあるディレクトリに移動して、ターミナルから以下のコマンドを実行します。

# 毎回docker runをタイプするのは面倒なので、コマンドエイリアスを登録する
$ alias rust-musl-builder='docker run --rm -it \
        -v "$(pwd)":/home/rust/src ekidd/rust-musl-builder'

# ビルドする
$ rust-musl-builder cargo build --release

# linux-musl向けのバイナリが生成された
$ ls -lh target/x86_64-unknown-linux-musl/release
-rwxr-xr-x  2 tatsuya  staff   4.1M Dec  7 16:19 hello-sqlite
-rw-r--r--  1 tatsuya  staff    97B Dec  7 16:19 hello-sqlite.d

極小のDockerイメージを作成

hello-sqliteバイナリの入ったDockerイメージを作成しましょう。

まず.dockerignoreファイルを作成して、あとでdocker buildコマンドを実行する際に不要なファイルが処理されないようにします。

$ echo 'target' > .dockerignore

Dockerfileを書きましょう。 Dockerのマルチステージビルドを使います。 最初のステージではrust-musl-builderでバイナリを生成し、次のステージはscratchという空のDockerイメージにバイナリをコピーします。

hello-sqlite/Dockerifle

# バイナリのビルド用にrust-musl-builderイメージを使用する
FROM ekidd/rust-musl-builder:stable AS builder

# カレントディレクトリ(このイメージでは/home/rust/src)にソースコードを追加する
ADD . ./

# ソースコードのパーミッションを調整する(macOSだとここで止まってしまうので
# コメントアウトしている)
# RUN sudo chown -R rust:rust /home/rust

# muslターゲット向けにバイナリをビルドする
# 必須ではないがstripコマンドでデバッグシンボルなどを削ぎ落とし、バイナリを小さくする
RUN cargo build --release && \
    strip /home/rust/src/target/x86_64-unknown-linux-musl/release/hello-sqlite

# ここからは実行用のイメージを作成する
# ベースイメージとしscratch(空のイメージ)を使用する
FROM scratch

# バイナリをルートディレクトリにコピーする
COPY --from=builder \
    /home/rust/src/target/x86_64-unknown-linux-musl/release/hello-sqlite /

# このイメージからコンテナを起動するとバイナリが実行されるようにする
ENTRYPOINT ["/hello-sqlite"]

実行用イメージのベースイメージにはscratchを使いました。 これは中身が空っぽのイメージです。

alpineをベースにしてもいいのですが、今回作成したプログラムはシェルなどのLinuxコマンドなどが不要なのでLinuxカーネルさえあれば動作します。 ですからscratchで十分です。

もちろん用途によってはalpineなど他のイメージの方が便利なこともあります。 たとえばOpenSSLに依存している場合はルート認証局(ルートCA)のファイルが必要です。 そういうときはベースイメージをalpineにして、パッケージ管理システムのapkでルートCAパッケージを追加するのが楽でしょう。 (一応、alpineからscratchへルートCAファイルをコピーするという技もあります)

ベースイメージ サイズ 用途
scratch 0 Rustバイナリといくつかのファイルだけがあればいいとき
busybox 約1.2MB シェルや基本的なUNIXコマンドが必要なとき
alpine 約5.6MB パッケージマネージャ(apk)が必要なとき

Dockerfileを元にDockerイメージを作成します。 イメージ名はhello-sqliteにしました。

$ docker bulid -t hello-sqlite .

イメージが作成できたら、Dockerコンテナを実行しましょう。

$ docker run --rm hello-sqlite
55    # ← 1から10までの数字の合計が表示された

イメージのサイズは約1.6MBと、とても小さくなりました。

$ docker images hello-sqlite
REPOSITORY    TAG     IMAGE ID      CREATED        SIZE
hello-sqlite  latest  81be816eaab0  2 minutes ago  1.62MB

$ docker images alpine:3.10
REPOSITORY    TAG     IMAGE ID      CREATED        SIZE
alpine        3.10    965ea09ff2eb  6 weeks ago    5.55MB

$ docker images ubuntu:18.04
REPOSITORY    TAG     IMAGE ID      CREATED        SIZE
ubuntu        18.04   775349758637  5 weeks ago    64.2MB

rust-musl-builderにないライブラリを使うには?

rust-musl-builderにはOpenSSLやlibpqなどWebアプリケーションに必要そうなライブラリがすでに入っています。 しかしアプリケーションによっては、これ以外の外部ライブラリが必要になることも少なくないでしょう。 そういうときはrust-musl-builderをベースにして、必要なライブラリを追加したイメージを作るのがお勧めです。

具体的なやり方については、rust-musl-builderリポジトリのadding-a-libraryの例(Dockerfile)を見てください。

前回と今回のまとめ

  • Rustのx86_64-unknown-linux-muslターゲットを使うと、libcを含む外部ライブラリに静的リンクしたバイナリが作成できる
  • ekidd/rust-musl-builderなどのDockerイメージを活用すれば、準備に時間をかけることなくmuslビルドが始められる
  • こうして作ったバイナリは、Alpine Linuxを含むさまざまなx86_64 Linux環境で実行できる
  • UbuntuやCentOSなどでも外部ライブラリをインストールしなくてすむので、気軽にバイナリを配布できる
  • Dockerのscratchイメージでも動作するので、極小のDockerイメージも作れる