Dockerfileを書かずに(嘘)Nixで圧倒的に軽量なDockerイメージを作成する(539MB->245MB->173MB)
Tags: nix, docker
Published: 2020-08-17 12:42:18

[Buildpacksの紹介記事](https://kotaroooo0-dev.hatenablog.com/entry/2020/08/15/183929)の便乗です．

[Nix](https://nixos.org/)はパッケージマネージャの一種で，決定的なビルドを提供することを主眼としています．この記事では，Nixを使って**更に小さいDockerイメージを構築する**ことを目指します．

# アプリのビルド
今回は元記事と同じくJavaアプリをビルドしてみます．
Nixはビルドの決定性を維持するために，アプリケーションの依存関係を厳密に追跡する必要があり，これにはアプリケーションの使用しているライブラリも含まれています．
そこで，Nixの世界では各プログラミング言語のパッケージマネージャの情報をNixに渡してあげるツールがよく用いられます．
今回はJavaなので，[mavenix](https://github.com/nix-community/mavenix)を使用します．

まずは，[Niv](https://github.com/nmattia/niv)を使ってmavenixをプロジェクトローカルにダウンロードします．
```bash
[~/buildpacks/samples/apps/java-maven]$ niv init
Initializing
  Creating nix/sources.nix
  Creating nix/sources.json
  Importing 'niv' ...
  Adding package niv
    Writing new sources file
  Done: Adding package niv
  Importing 'nixpkgs' ...
  Adding package nixpkgs
    Writing new sources file
  Done: Adding package nixpkgs
Done: Initializing

[~/buildpacks/samples/apps/java-maven]$ niv add nix-community/mavenix
Adding package mavenix
  Writing new sources file
Done: Adding package mavenix
```

次の`shell.nix`を使ってmavenixがインストールされた開発環境を構築します．Nixではプロジェクトごとの開発ツールをグローバルにインストールする必要が無いのです！

```nix
# shell.nix
{ sources ? import ./nix/sources.nix }:
let
  pkgs = import sources.nixpkgs {};
  mavenix = (import sources.mavenix { inherit pkgs; }).cli;
in
  pkgs.mkShell {
    buildInputs = [
      mavenix
    ];
  }
```

開発環境の起動には`nix-shell`を使いましょう．

```bash
[~/buildpacks/samples/apps/java-maven]$ nix-shell
```

これでmavenixが使えるようになりました．`mvnix-init`で`default.nix`を生成します．`default.nix`はNixにアプリケーションをビルドする方法を伝えるためのファイルです．今回の場合はmavenixによってMavenアプリ用のテンプレートが出力されます．

```bash
[~/buildpacks/samples/apps/java-maven]$ mvnix-init 

  Creating files:

./default.nix

  1. Configure by editing './default.nix'
  2. Create a lock file by running 'mvnix-update "./default.nix"'
  3. Build your project 'nix-build "./default.nix"
```

このインストラクションに従うとアプリのビルドが完了します．
```bash
[~/buildpacks/samples/apps/java-maven]$ mvnix-update
...時間がかかる...
[~/buildpacks/samples/apps/java-maven]$ nix-build --no-out-link
nix/store/ph7mw7lja1i3b8m7lwa1746r297l5bg8-sample-0.0.1-SNAPSHOT
```

出力されたパスにビルド結果が出力されているのを確認しましょう．
```bash
[~/buildpacks/samples/apps/java-maven]$ ls -lh /nix/store/ph7mw7lja1i3b8m7lwa1746r297l5bg8-sample-0.0.1-SNAPSHOT/share/java/
total 18M
-r--r--r-- 1 root root  18M Jan  1  1970 sample-0.0.1-SNAPSHOT.jar
-r--r--r-- 1 root root  137 Jan  1  1970 sample-0.0.1-SNAPSHOT.metadata.xml
-r-xr-xr-x 1 root root 1.4K Jan  1  1970 sample-0.0.1-SNAPSHOT.pom
-r--r--r-- 1 root root   72 Jan  1  1970 sample-0.0.1-SNAPSHOT.properties
```

ローカルで起動することもできます．
```bash
[~/buildpacks/samples/apps/java-maven]$ nix-shell -p jdk # OpenJDKを一時的にインストール
[~/buildpacks/samples/apps/java-maven]$ java -jar /nix/store/ph7mw7lja1i3b8m7lwa1746r297l5bg8-sample-0.0.1-SNAPSHOT/share/java/sample-0.0.1-SNAPSHOT.jar
```

# Dockerイメージのビルド
それでは，このアプリケーション用のDockerコンテナを作成しましょう．これもNixを使うと簡単です！
コンテナ内にJavaを入れたいので，`default.nix`を修正します．

```nix
# default.nix
# This file has been generated by mavenix-2.3.3. Configure the build here!
let
  mavenix-src = fetchTarball { url = "https://github.com/nix-community/mavenix/tarball/v2.3.3"; sha256 = "1l653ac3ka4apm7s4qrbm4kx7ij7n2zk3b67p9l0nki8vxxi8jv7"; };
in {
  # pkgs is pinned to 19.09 in mavenix-src,
  # replace/invoke with <nixpkgs> or /path/to/your/nixpkgs_checkout
  pkgs ? (import mavenix-src {}).pkgs,
  mavenix ? import mavenix-src { inherit pkgs; },
  src ? ./.,
  doCheck ? false,
}: mavenix.buildMaven {
  inherit src doCheck;
  infoFile = ./mavenix.lock;

  buildInputs = [ 
    pkgs.jre_headless
    pkgs.makeWrapper
  ];

  postInstall = ''
    # install java
    makeWrapper ${pkgs.jre_headless}/bin/java $out/bin/java \
      --add-flags "-jar $out/share/java/sample-0.0.1-SNAPSHOT.jar"
      
    # create /tmp for Spring framework
    mkdir $out/tmp
    chmod 777 $out/tmp
  '';
}
```

ここでは，`buildInputs`のところでにJREを追加し，`postInstall`フックでJavaバイナリを`/bin`に配置しています．また，Springが実行時に`/tmp`に書き込むのでディレクトリを作成しています．

次の内容で`Dockerfile.nix`を用意します．ハイ，`Dockerfile`は書かなくともNixファイルが必要なのです．
```nix
{ sources ? import ./nix/sources.nix }:
let
  pkgs = import sources.nixpkgs {};
  app = import ./default.nix {};
in
  pkgs.dockerTools.buildImage {
    name = "example";
    tag = "latest";

    contents = app;

    # エントリポイント
    config.Cmd = [
      "/bin/server"
    ];
  }
```

標準のDockerfileではなく，Nixを使ってDockerイメージを作成しています．Dockerfileでは**操作**(`apt install`)を記述するのに対し，Nixを使った場合は**内容**を記述します．
今回の場合は先程作成したアプリのパッケージ(`./default.nix`)**だけ**をコンテナ内に持つように指定しています．
ビルド方法はアプリのときと同じです．

```bash
[~/buildpacks/samples/apps/java-maven]$ nix-build Dockerfile.nix --no-out-link                                                                                                                                                 
/nix/store/b8l5qssgiylc8yi4f8y2ybz83l4nqbrp-docker-image-example.tar.gz
```

このイメージは`docker load -i`で読み込めます．

```bash
[~/buildpacks/samples/apps/java-maven]$ docker load -i /nix/store/b8l5qssgiylc8yi4f8y2ybz83l4nqbrp-docker-image-example.tar.gz                                                                                                 
66dd4ba45ad5: Loading layer [==================================================>]  174.5MB/174.5MB                                                                                                                                           
The image example:latest already exists, renaming the old one with ID sha256:76903c2f6febfd083934876f42a64e0bfd18442e80093b3c1021c97d81a35825 to empty string                                                                                
Loaded image: example:latest
```

実行してみましょう．

```bash
[~/buildpacks/samples/apps/java-maven]$ docker run -p 8080:8080 example:latest
```

やりました！
サイズも**173MB**と上出来ですね😎

```bash
[~/buildpacks/samples/apps/java-maven]$ docker images | grep example                                                                                                                                                           
example latest 4c866b80b51a 50 years ago 173MB
```