nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【Rust】【Docker】Cargoプロジェクトで素直に書いたDockerfileをdocker buildするとソースが書き換わるたびにフルビルドが走って滅茶苦茶遅いのを対策する方法を改良してもっとキャッシュさせる。

目次

ソース

RustのCargoプロジェクトで素直に書いたDockerfileをdocker buildするとソースが書き換わるたびにフルビルドが走って滅茶苦茶遅いことはcargoのファイルだけコピーしてビルドすることで解決します - ncaq

問題点

記事内でもあるが、

この方法を使っても,自分のアプリケーションは差分ビルドすることは出来ません またライブラリのバージョンを1つだけ上げるような行為を行っても当然フルビルドになってしまいます.

これがめっちゃしんどいのに対する対策が今回の議題。

おさらい

元々、任意のRustプロジェクトに対し、

COPY . .
RUN cargo build --release

素直にこれをやるとフルビルドが走るよねってことで、

# プログラムの依存関係だけをコピー
COPY Cargo.toml Cargo.lock /work-dir/
# 何もプログラムが無いとビルドエラーになるのでダミーのものを用意する
RUN mkdir -p /work-dir/src/ && touch /work-dir/src/lib.rs

# キャッシュのために依存ライブラリだけをビルドする
RUN cargo build --release

# リポジトリ全体をコピー
COPY . .

# 本物のビルドを行う
RUN cargo build --release

これをやると依存関係のビルドだけ行われた・・・かに思われるが、 これには罠があり、Cargo.toml Cargo.lockが書き変わると2行目からやり直しになる。

これが例えばCIとかであれば独自のキャッシュ機構とかを使ったりして問題無いかもしれないが、dockerのシンプルなキャッシュを使ってローカルでビルドする事はできない。

これは面倒だ。

対策:ダミーのCargo.toml Cargo.lockを作ってDockerfileに仕込んでおく。

これは泥臭いアプローチである。簡単に言えば、docker(正確にはbuild kitか?)を騙す的なアプローチ。

github.com

  • dummy-cargo-toml-createrを実行してCargo.toml Cargo.lockのパッケージのバージョンだけを0.1.0に固定したDummyVersion.tomlとDummyVersion.lockに作る。
$ cargo install dummy-cargo-toml-creater
$ ~/.cargo/bin/dummy-cargo-toml-creater
$ ls ./DummyVersion.toml
./DummyVersion.toml
$ ls ./DummyVersion.lock
./DummyVersion.lock

この操作は、Makefileとか使ってるなら

dummy-cargo-toml-create:
    ~/.cargo/bin/dummy-cargo-toml-creater

docker-build: dummy-cargo-toml-create
    docker build dummy .

とかしておけば忘れない。

  • 依存パッケージのビルドの時にそのDummyVersion.tomlとDummyVersion.lockをCargo.toml Cargo.lockとしてDockerに入れ込んでビルドする。

  • 最後に ./src、そしてCargo.tomlとCargo.lockを入れ込んでビルドする。

最終的に ekidd/rust-musl-builder を使ったりした時はこうなる。

FROM ekidd/rust-musl-builder:nightly-2019-04-25 as builder

## Build Cache Dependency Library
RUN mkdir /tmp/app
WORKDIR /tmp/app
## Build Dependency Library with DummyVersion.toml/lock
COPY DummyVersion.toml ./Cargo.toml
COPY DummyVersion.lock ./Cargo.lock
RUN mkdir -p src/ && \
    touch src/lib.rs
RUN sudo chown -R rust:rust .
RUN cargo build --release
## Build Base Library with Cargo.toml/lock
COPY ./src/ ./src/
COPY Cargo.toml ./Cargo.toml
COPY Cargo.lock ./Cargo.lock
RUN sudo chown -R rust:rust .
RUN cargo build --release

これでdocker buildをすれば、例え Cargo.tomlのpackage.versionを書き換えても依存パッケはキャッシュされ続けるし、それ以外が書き換わった時はしっかりそのキャッシュは使われなくなる。

dummy-cargo-toml-createrは何をしている?

fn ctoml_creater() {
    let manifest_str = match File::open("./Cargo.toml") {
        Ok(file) => {
            let mut buf_file = BufReader::new(file);
            do_cat(&mut buf_file)
        },
        Err(e) => panic!("{}", e),
    };
    let mut doc = manifest_str.parse::<Document>().expect("invalid doc");
    doc["package"]["version"] = value("0.1.0");
    let mut file = std::fs::File::create("./DummyVersion.toml").unwrap();
    file.write_all(doc.to_string().as_bytes()).expect("Could not write to file!");
}

fn clock_creater() {
    let manifest_str = match File::open("./Cargo.lock") {
        Ok(file) => {
            let mut buf_file = BufReader::new(file);
            do_cat(&mut buf_file)
        },
        Err(e) => panic!("{}", e),
    };
    let mut doc = manifest_str.parse::<Document>().expect("invalid doc");
    let mut counter = 0;
    let mut idx = 0;
    let tables = doc["package"].as_array_of_tables_mut().unwrap();
    for t in tables.iter() {
        for v in t.iter() {
            if v.0 == "name" {
                if let Some(s) = v.1.as_str() {
                    if s ==  env!("CARGO_PKG_NAME") {
                        idx = counter;
                        break
                    }
                }
            }
        }
        counter += 1;
    }
    doc["package"][idx]["version"] = value("0.1.0");
    let mut file = std::fs::File::create("./DummyVersion.lock").unwrap();
    file.write_all(doc.to_string().as_bytes()).expect("Could not write to file!");
}

https://github.com/nnao45/dummy-cargo-toml-creater/blob/master/src/main.rs

やってることは、

  1. Cargo.tomlとCargo.lockを読み込んでStringで持つ。
  2. manifest_str.parse::<toml_read::Document>()を使って toml_read::Document 型にパースする。
  3. 2で作ったオブジェクトの持つtoml::Valueに頑張ってたどり着いて、書き換える
  4. 書き換えたオブジェクトをDummyVersion.tomlとDummyVersion.lockとして保存する。

3の時の Cargo.lock だが、正直めっちゃイケてない・・・が、どうやってスマートにpackageというtomlの配列テーブルから自分のパッケージを探し出して書き込むのか考えるのが面倒だったから途中でやめた・・・・機能は満たしているからいいだろ感。

もっと言えば、別にrustでこの機能を書く必要さえ無いと言えば無い・・・笑 が、今回のrustのビルドが目的なので、まぁrustで書くかとなった。

最後に

正直綺麗な実装とは言えないが、キャッシュも依存の更新も、パッケージ自体のバージョンアップにも対応していて上手くいく。dummy-cargo-toml-createrも大したことやってないので、一つのアプローチとして是非使いたい人はカスタマイズして各々の開発環境に即してキャッシュを効かせてみて欲しい。

流石に、cargo1.40くらいには対応されそうなもんだが・・・。