yamlで保存してあるconda仮想環境をDocker上で構築

## (追記 2022-11-05)

本記事の内容(オリジナルは2020年の7月頃)ですが、
2年以上経って久々に見返してみると我ながら色々とよろしくない点もあったように思うので、
可能な範囲で直した改善版: [yamlで保存してあるconda仮想環境をDocker上で構築(multistage-build)](https://qiita.com/junkor-1011/items/9913de762f2292499b82)を本記事の続きとして書いています。

本記事だとdocker内で作成しているユーザーの権限が大き過ぎたり、minicondaを使っている関係で商用利用が有償化されているAnacondaリポジトリから意図せずにパッケージを取得してしまう可能性があったりと、そのまま実行すると懸念点があるので注意してください。

---

Docker上でPython環境を作るときに`pip`や`Pipenv`なども使うのだが、使うライブラリやパッケージの関係で`conda`環境を使えると便利なことがある。
思った通りのconda環境をDocker上で作るための方法の自分用メモ。

## 1. 動機

Pythonの開発やデータ分析などでMinicondaやAnacondaを使うことがある。
（依存ライブラリも割と一緒にインストールしてくれるので自分で入れなくて良いなど環境構築が楽だったり、バイナリ互換性の問題でcondaの特定チャンネルからのインストールを推奨されるパッケージがあったりする）
その際、やっている作業毎にパッケージやバージョンを変えたかったり、環境を後で再現しやすくするなどの目的で基本はconda仮想環境を使うようにしている。
が、Python以外のところから環境設定・構築が必要になることがあったり、仮想環境と言えどもPCの環境を汚して他と干渉したりの問題が起きるなどで、結局Dockerで対応することが多いため。（で、そのDockerへの移植時に結構苦戦したため）

#### yamlファイルを使う理由

詳細は[次章](#2-conda環境の保存と再構築)として、

- PC上の既存のconda仮想環境を容易にDocker上に移植出来る
- 逆に、Docker上で開発したconda環境を手元のAnacondaやMinicondaですぐに再現出来る

など

## 2. conda環境の保存と再構築

### 2-1. yamlファイルからのconda環境生成

[（公式ドキュメント）](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file)

```bash
conda env create -f environment.yml
```
を実行すると、`environment.yml`に記載した情報の通りにconda環境が生成される。
例えば`environment.yml`に対応するものをgit上などで管理しておくと、可搬性や再現性の点で便利。

※`environment.yml`の中身は以降の記述を参考
※`conda env create`以降のオプションの詳細ははっきり覚えていない。。。発見出来てかつ気が向いたら後で追記。ちなみに`conda create`のオプションは[こんな感じ](https://docs.conda.io/projects/conda/en/latest/commands/create.html)。

### 2-2. yamlファイルの生成

[（公式ドキュメント）](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#exporting-the-environment-yml-file)

対象のconda仮想環境に入って、例えば以下のようにする

```bash
conda env export > environment.yml
```
すると、該当のconda環境でインストールされていた全パッケージとそのバージョン（ビルド情報含む）が`environment.yml`に書き出される。(`pip freeze`するような感覚)

ちなみに、仮想環境に入らなくとも

```bash
conda env export  -n <仮想環境名> > environment.yml
```
のようにして、明示的に書き出す仮想環境名を指定することも出来る。

また、上記の2つは完全にバージョン情報が固定されるが、そこまではせずに最低限の条件だけ指定したい場合がある。
[（公式ドキュメント）](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually)を参考にして、パッケージと使うチャンネルなどを書いたyamlファイルを自分で作ることが出来る。（以下、適当な例）

```YAML:create_env_sample.yml
name: hoge
channels:
  - conda-forge
  - default
dependencies:
  - python==3.7.*
  - pandas<1.0
  - flask
  - pip
  - pip:
    - pynvim
```
- 依存ライブラリなどは（conflictが起きなければ）自動でインストールされる。
- pipでインストールされるものも書ける。（`pip:`の下のところ）
    - 先にcondaでのインストールが実行され、最後にpipでのインストールが実行される
    - condaでインストールするパッケージ内で明示的にpipをインストールしておくと無難かもしれない
        - 「conda仮想環境のものでないpipを使う可能性がある」的な趣旨のwarningが出る

なお、上記の例のyamlファイルだと、

```bash
# condaでパッケージをインストール
conda create -n hoge python=3.7 pandas\<1.0 flask pip -c conda-forge
# 作った仮想環境に入る
conda activate hoge
# 最後にpipで必要なパッケージをインストール
python -m pip install pynvim
```
みたいな感じで環境構築するのと大体同じ。（※厳密に本当に同じかまでは未検証）

## 3. Docker上でconda仮想環境を作る

### (方針)
- [前章](#2-conda環境の保存と再構築)を踏まえて用意してあるyamlファイルをDockerイメージ内に`COPY`などしておく
- yamlファイルからの環境作成はMinicondaを利用して行う
    - Miniconda自体のインストールは[公式](https://docs.conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent)などを参照
    - continiuum公式の[Dockerfile](https://hub.docker.com/r/continuumio/miniconda/dockerfile)なども参考になる
- 上記のMinicondaはyamlファイルから仮想環境を作ること「のみ」に使い、**作り終わったら元のMinicondaはキャッシュ毎全部消す**（←容量の無駄なので）
    - 代わりにyamlから作られた仮想環境の方を`base`にする
    - （もっと良いやり方があるかもしれない）

### Dockerfile作成例（抜粋）

ディレクトリ構成:

```bash
tree . 
# .
# ├── Dockerfile
# └── environment.yml
```
のような感じ。


```bash
docker build --tag=hoge .
```
または

```bash
# Dockerfile内のARG部分を制御する場合
docker build \
    --tag=hoge \
    --build-arg VAL1=FOO \
    --build-arg VAL2=BAR \
     . 
```
のようにすると、ビルドが通れば`hoge`というイメージが出来る

```Dockerfile:Dockerfile
ARG BASE_IMAGE=ubuntu:latest
FROM ${BASE_IMAGE}

# system update & package install
RUN apt-get clean && \
    apt-get -y update && \
    apt-get install -y --no-install-recommends \
    unzip bzip2 \
    openssl libssl-dev \
    curl wget \
    ca-certificates \
    locales \
    bash \
    sudo \
    git \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*


# ローカルuser作成
ARG USER_NAME=user
ARG USER_UID=1000
ARG PASSWD=password
RUN useradd -m -s /bin/bash -u ${USER_UID} ${USER_NAME} && \
    gpasswd -a ${USER_NAME} sudo && \
    echo "${USER_NAME}:${PASSWD}" | chpasswd && \
    echo "${USER_NAME} ALL=(ALL) ALL" >> /etc/sudoers && \
    chmod g+w /etc/passwd


# conda用準備
ENV CONDA_DIR=/opt/conda \
    CONDA_TMP_DIR=/tmp/conda \
    HOME=/home/$USER_NAME \
    SHELL=/bin/bash
RUN mkdir -p $CONDA_DIR && \
    mkdir -p $CONDA_TMP_DIR && \
    chown $USER_NAME:$USER_UID $CONDA_DIR && \
    chown $USER_NAME:$USER_UID $CONDA_TMP_DIR
# yamlファイルの取り込み
ARG CONDA_YAML="./environment.yml"
COPY $CONDA_YAML /tmp/conda_packages.yml

USER ${USER_NAME}

WORKDIR $HOME
# miniconda
ARG MINICONDA_VERSION=py37_4.8.3-Linux-x86_64
ARG MINICONDA_MD5=751786b92c00b1aeae3f017b781018df
ENV PATH=${CONDA_DIR}/bin:$PATH

RUN cd /tmp && \
    wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-${MINICONDA_VERSION}.sh && \
    echo "${MINICONDA_MD5} *Miniconda3-${MINICONDA_VERSION}.sh" | md5sum -c - && \
    /bin/bash Miniconda3-${MINICONDA_VERSION}.sh -f -b -p $CONDA_TMP_DIR && \
    rm Miniconda3-${MINICONDA_VERSION}.sh && \
    $CONDA_TMP_DIR/bin/conda env create -f /tmp/conda_packages.yml -p $CONDA_DIR && \
    rm -rf $HOME/.cache/* && \
    rm -rf $CONDA_TMP_DIR/*

# （以下省略）
```
- はじめにMinicondaは`/tmp/conda/`下にインストールする
    - 先述の通り、`conda env create`をするためだけに使うので、後で`/tmp/conda/`下をまるっと消す
    - インストール先は`-p`オプションで指定している
- 所望のconda環境は`/opt/conda/`下にインストールされ、ここに`PATH`を通すことで作成ユーザーにとってのデフォルトPython環境にしている
    - conda環境を`/opt/conda/`にするために、`-p`や`--prefix`オプションでインストール先を指定
- イメージサイズが肥大化しないように、Minicondaのダウンロード・インストールから不要なファイル・キャッシュの削除まで**1つの`RUN`の中で全てをやり切る必要**

#### (補足)
- 使っているMinicondaは執筆時点（2020-07-24）での最新版なので、[ここ](https://repo.anaconda.com/miniconda/)などを参考に適宜アップデートする
- ユーザー作成を行っているが別に必須ではないので、そこを省けばもっとすっきりするはず
- yamlファイルにおける`prefix:`部分があればそれはインストール場所を示すが、`conda env create`の`-p`ないし`--prefix`オプションでの指定が優先される様子
- yamlファイルに書くインストールパッケージの中に`conda`を書いておくと、`/opt/conda/`にインストールされる仮想環境が新しい`base`環境になる
    - コンテナ使用中に追加で入れたいパッケージが出てきたときにその場で`conda install ***`を実行出来たり、その後`conda env export -n base | tee environment.yml`などと叩いてインストールパッケージの再確認や新しいyamlファイルの生成が出来る

##### (更に細かい補足)

たまに`conda env export`で生成したyamlファイルをそのまま`conda env create`に渡せないときがある（あった）ので、その例と対処。

- `pip`で`graphviz`をインストールさせたものを`conda env export`してyamlを生成すると、`python-graphviz`の名前になっている
- しかし、`pip install python-graphviz`はエラーになるため、したがってこのyamlファイルをそのまま使うとインストールエラーになってしまう
- そこで、例えば以下の部分を挿入してyamlファイルを少しだけ書き換えるなどした

```Dockerfile
RUN sed -i -e 's/python-graphviz/graphviz/' /tmp/conda_packages.yml
```
`sed`でyamlファイル内の`python-graphviz`を`graphviz`に置換している

## 参考

conda環境の保存・構築：
https://qiita.com/nshinya/items/cb1cffabc3305c907bc5
https://qiita.com/yubessy/items/2dd43551aa8308dc7eca

Docker上でのconda環境の構築
https://hub.docker.com/r/continuumio/miniconda/dockerfile
https://github.com/jupyter/docker-stacks/tree/master (←リポジトリ内の各Dockerfileを参照)