yamlで保存してあるconda仮想環境をDocker上で構築(multistage-build)


昔書いた記事: [yamlで保存してあるconda仮想環境をDocker上で構築](https://qiita.com/junkor-1011/items/cd7c0e626aedc335011d)をふと見返してみると、色々と微妙というか「個人的には今ならこうは書かないな。。。」と思ったのでアップデート版を書いてみる。


## 概要

前提知識:

- [conda cli](https://docs.conda.io/projects/conda/en/latest/commands.html)の基本と概念
    - 特にAnacondaやminicondaで仮想環境を作ることができ、yamlファイルの形でexport出来ること
    - 逆に、yamlファイルを取り込むことでAnacondaやminicondaなどで仮想環境を作成出来ること
- docker周りの基本
- linux周りの基本

**やること:** 
- [`conda env export`コマンドなどから生成したyamlファイル](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#exporting-an-environment-file-across-platforms)や、[それと同じ形式で手作業などで作成したyamlファイル](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually)からdockerで動作するconda環境を作る
    - せっかくDockerでやるので、なるべく無駄なものは入れずミニマルに作る方針

**前回からの主な改善点**

- **[multistage-build](https://docs.docker.jp/develop/develop-images/multistage-build.html)を使う**
    - ツールのビルドなど下準備用のLayerを実際に動かすイメージから分離出来るため、余計なものをインストールしなくて良くなったり、キャッシュファイルの削除を頑張らなくても良くなったりする。すなわち、イメージサイズの削減につながるし、余計なものが無い方がセキュリティ的にも良いと思われる。
- conda環境の生成に[mamba](https://github.com/mamba-org/mamba)を使っている
    - `conda`のC++による再実装(githubのREADMEより)であり、conda互換でかつ高速(ビルド時間の削減)
- conda環境の生成に[mambaforge](https://hub.docker.com/r/condaforge/mambaforge)イメージを利用している
    - Anacondaリポジトリの商用利用が有償化されており、minicondaを使っているとdefault channel(=Anaconda公式リポジトリ)からパッケージを取得してしまう可能性があった気がするので、商用利用にあたる場合、使うyamlファイルで全パッケージリポジトリレベルまで指定されていない場合はminicondaを使わないほうが無難と思われる
    - [miniforge](https://github.com/conda-forge/miniforge)はやや不正確ながらざっくりいうとdefault channelが[conda-forge](https://anaconda.org/conda-forge)になっている`miniconda`のようなもの。[mambaforge](https://github.com/conda-forge/miniforge#mambaforge)はそれの`mamba`コマンド(先述)が使えるバージョン
    - あと、前回はminicondaのインストールから始めていたので、今回は最初から`mamba`が使えるイメージを使うことで記述内容をかなり簡略化できた
- (本質では無いが)docker内で使うユーザーの権限見直し
    - 以前の記事ではrootへの昇格を許していたが、通常はやらない方が良いので改めている


## 改善後の環境生成方法

今回は以下のyamlファイル:

```yaml:conda_env.yaml
name: example_env
channels:
  - conda-forge
dependencies:
  - python==3.10.*
  - pandas
  - jupyterlab
  - pip
  - pip:
    - python-language-server[all]
```

を使ってconda環境を作る。
内容は割とナンセンスで本質的では無いので注意。

使うDockerfileは例えば以下のような感じにする。

```dockerfile:Dockerfile
FROM condaforge/mambaforge:latest as builder
COPY conda_env.yaml .

RUN mkdir -p /usr/local/conda && \
    mamba env create -f ./conda_env.yaml -p /usr/local/conda

# 今回の例ではベースイメージにubuntuを使う。(mambaforgeイメージのベースも執筆時点でubuntuだったりする)
FROM ubuntu:latest as main

# NOTE: 環境によって設定すべき値は変わるので、必要があれば編集する
# または、ARGとしているのでbuild時に値を与えて変更する
ARG USERNAME=user
ARG GROUPNAME=user
ARG UIDVALUE=1000
ARG GIDVALUE=1000

# NOTE: 使いたいパッケージがあれば適宜追加する
RUN apt-get update && \
    apt-get install --no-install-recommends --yes \
    tini && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# rootユーザーで動かすのは避けたいのでユーザーを作成する
# 設定するユーザー名やUIDなどは必要に応じて上記のARGの値で変更する
RUN groupadd --gid  ${GIDVALUE} ${GROUPNAME} && \
    useradd --uid ${UIDVALUE} --gid ${GROUPNAME} --shell /bin/bash --create-home ${USERNAME}

# この例では/usr/local/condaディレクトリ上にconda環境を格納する
RUN mkdir -p /usr/local/conda
# builderイメージからそっくりそのままコピーする(所有権は変えておく)
COPY --from=builder --chown=${UIDVALUE}:${UIDVALUE} /usr/local/conda/ /usr/local/conda

# 実行ユーザーをrootから先程作成したものに切り替え
USER ${UIDVALUE}

# 作成したconda環境にPATHを追加する
ENV PATH=/usr/local/conda/bin:${PATH}

# ----------------------------------------------------------------------------------------------- #

# ここから下は特にユースケース次第で完全に変わる部分なので適当に編集すること。
# 今回はそこそこ実用的な例?としてjupyterlabを起動している
# なお、例なのでjupyterの起動オプションでtoken=''(ログイン認証無し)としているが、むやみにやるのは良くない
WORKDIR /home/${USERNAME}
EXPOSE 8888
ENTRYPOINT ["tini", "--"]
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--NotebookApp.token=''"]
```
multi-stage buildを使ったことなどにより、[前回記事](https://qiita.com/junkor-1011/items/cd7c0e626aedc335011d)よりも簡潔に記述できている。（トリッキーな部分を大幅に少なくできている）

なお、作成するconda環境内の`bin`ディレクトリだが、一部のコマンドは絶対パスの参照をもつシンボリックがある？ようで、
builderとmainとでconda環境の置き場所の絶対パスを同じにしないと動かなかったので、編集して使う際は注意。


これで、上記の`Dockerfile`と`conda_env.yaml`を置いてあるディレクトリで

```sh
docker build . -t <タグ名>
```
などとすればイメージが作れる。

なお、厳密に条件を揃えた訳では無いが、同一の`conda_env.yaml`を使って作成したDockerイメージのサイズを比較したところ、執筆時点(2022/11/4)では[前回記事](https://qiita.com/junkor-1011/items/cd7c0e626aedc335011d)の方法で850MB、今回の方法では753MBとなった。
思ったよりもイメージサイズを削減できていることがわかった。ちゃんと調べていないが、前回記事はminicondaをインストールして動かしたりするために`apt-get`などで入れないといけないパッケージが若干あったため、主にその分で差が出ている？と思われる。~~（正直どちらも割と大きいが）~~
（↑今回の例ではなんとなく[tini](https://github.com/krallin/tini)を入れているが必須のものではなく、aptなどでマストで追加しないといけないパッケージは実質存在しない）


## 感想

- multi-stage buildは便利
- (正直ここ最近ほぼ全くPythonとかcondaとか触っていないので、知識のアップデート漏れがあったら申し訳ないです)