この記事は僕の以下のブログからの転載です。
イテレーションの速さがあなたの生産性を左右する。どうも、かわしんです。生産性の高いプログラマって1つ1つの試行が素早い(自動化しているかツールを使っている)ためにものすごいスピードで開発できていると思うんですよね。
さて、最近 Python で開発をしているのですが、世の中の Docker と Pipenv の開発環境を調べてもろくなものがなかったので、自分でテンプレートを作りました。いわゆる「俺の考える最強の Pipenv + Docker 開発環境」というやつです。
リポジトリはこちらになります。
特徴としては、以下の2つが大きいです。
-
pipenv install
をコンテナ起動時に行うため、docker イメージを作り直す必要がない -
pipenv shell
相当の仮想環境のアクティベートを自動で行う
なぜ Docker + Pipenv なのか
Docker は、pypy パッケージ以外のライブラリの環境(apt instal
するもの)を隔離して複数の開発者間で再現させるために必要です。
Pipenv は Pipfile
と Pipfile.lock
を使ったバージョン固定の仕組みと packages
と dev-packages
の分離の機能があるため、pip ではなく Pipenv を利用したいです。Docker を使う場合環境の分離は達成されるため、Pipenv の仮想環境の仕組みは必要ないですがモダンなパッケージ管理機能を提供するものは Pipenv しかないのでこれを使いたいです。
つまり、Docker と Pipenv のそれぞれを利用する合理的なメリット があります。(docker 使わなくていいじゃ〜んとか、pip でいいじゃ〜んとかは言わないでください)
Pipenv の辛いところ
Pipenv の辛いところは install
や update
や lock
の仕組みが直感に反していて使いづらいとか、lock
が遅すぎるとか、重複した install
のスキップが遅いとかありますが、一番の辛いところは、仮想環境の virtualenv の仕組みとパッケージ管理の仕組みが密結合している ということにあります。
一応 pipenv install --system
というコマンドがありシステムグローバルにパッケージをインストールするオプションがあり、世の中の pipenv と docker の環境の説明記事ではこれを利用していますが、開発環境としての利用には全く向いていません。
確かに docker と virtualenv の 2 重の仮想環境を避けることができますが、pipenv lock
pipenv graph
などのパッケージ管理の機能は virtualenv による仮想環境を必須としており、新しいパッケージをインストールするときに 結局仮想環境が作られ、2 重にパッケージのインストールがされてしまいます 。
既存の pip はモダンなパッケージ管理ツールとしては機能不足(packages
と dev-packages
の分離やパッケージのゆるいバージョン指定と lock ファイルの仕組みなど)、pipenv は 仮想環境機能との密結合 とそれぞれのコマンドが遅い(lock
が致命的に遅い)など、Python のパッケージ管理ツールには満足いくものがない ので、誰かモダンな Python のパッケージ管理ツールを作れば流行ると思います。(Vendoring の仕組みを使えばできるはずです。Ruby の bundler とかで達成されているもの)
解決策
そこで僕の考え出したのが以下の環境です。
特徴を列挙しておきます。
- 開発環境では、Docker の中に pipenv の仮想環境を作る
- 開発環境なのでオーバーヘッドは許容できる。また、
pipenv graph
などのコマンドを満足に使えるメリットの方が大きい。
- 開発環境なのでオーバーヘッドは許容できる。また、
-
docker-compose build
では pypy パッケージのインストールは行わず、ENTRYPOINT でインストールする。virtualenv の環境を Docker Volume に載せる- 新しいパッケージのインストールでいちいち docker イメージを作り直す必要がなくなる。
- Docker Volume にインストールしたパッケージが載っているのでコンテナを作り直してもインストールし直す必要がない。
- 高速に気軽にライブラリインストールを試すイテレーションを回せる
- 自動で pipenv の仮想環境をアクティベートする
-
docker-compose run
でもdocker-compose exec <service> bash
でも、もちろんdocker-compose up
でも、pipenv の仮想環境内で実行される。 - いちいち
pipenv shell
などをして仮想環境のアクティベートをする必要はない。 - pipenv のアクティベートを気にしなくてよくて、めんどくさくない
-
ではファイルを列挙していきます。
docker-compose.yml
version: "3"
services:
app:
build:
context: .
dockerfile: "Dockerfile.dev"
command: python main.py
volumes:
- .:/app
- python-packages:/root/.local/share
volumes:
python-packages:
開発環境のための Dockerfile.dev
を利用します。
あと、docker volume を virtualenv の仮想環境が配置される /root/.local/share
に割り当てています。
Dockerfile.dev
FROM python:3.7.4
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
git \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
ENV ENTRYKIT_VERSION 0.4.0
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& mv entrykit /bin/entrykit \
&& chmod +x /bin/entrykit \
&& entrykit --symlink
WORKDIR /app
RUN pip install --upgrade pip && pip install pipenv
RUN echo "if [[ -z \"\${VIRTUAL_ENV}\" ]]; then" >> /root/.bashrc && \
echo "source \$(pipenv --venv)/bin/activate" >> /root/.bashrc && \
echo "fi" >> /root/.bashrc
COPY scripts/ /opt/bin/
ENTRYPOINT [ \
"prehook", "/opt/bin/docker-setup.sh", "--", \
"/opt/bin/docker-entrypoint.sh"]
ENTRYPOINT のフックに Entrykit を利用しています。別にこれでなくてもいいのですが、docker stop
の SIGTERM
を実行プロセスに伝搬させるために利用しています。
ENTRYPOINT では /opt/bin/docker-setup.sh
で pipenv install
を、/opt/bin/docker-entrypoint.sh
で virtualenv の仮想環境のアクティベートを行なっています。
また、.bashrc
に、source \$(pipenv --venv)/bin/activate
を設定しています。docker-compose exec
をした時は ENTRYPOINT が回避されてしまうので仮想環境のアクティベートを .bashrc
でフックして行います。(そのため、docker-compose exec
で bash
以外が実行されるとアクティベートされなくなりますが、基本的に開発環境で exec
する時は bash
以外ないのでこれでよしとしています。)
しかし、docker-compose run
したときに bash
を実行されると /opt/bin/docker-entrypoint.sh
と .bashrc
で 2 重にアクティベートされてしまいます、これを防ぐために VIRTUAL_ENV
という環境変数を確認しています。
scripts/docker-setup.sh
#!/usr/bin/env bash
pipenv --venv > /dev/null || pipenv install --skip-lock --dev --ignore-pipfile
コンテナの初回起動時にだけ pipenv install
を行います。それ以降の起動時には、pipenv --venv
に成功するので pipenv install
をスキップすることができます。(すでにインストール済みの場合でも pipenv install
は遅いのでスキップします)
scripts/docker-entrypoint.sh
#!/usr/bin/env bash
if [[ -z "${VIRTUAL_ENV}" ]]; then
source "$(pipenv --venv)/bin/activate"
fi
exec "$@"
virtualenv のアクティベートを行なってから command
に指定されたものを実行しています。
Dockerfile
FROM python:3.7.4-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
git \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
ENV WORKDIR /app/
WORKDIR ${WORKDIR}
COPY Pipfile Pipfile.lock ${WORKDIR}
RUN pip install pipenv --no-cache-dir && \
pipenv install --system --deploy && \
pip uninstall -y pipenv virtualenv-clone virtualenv
COPY . $WORKDIR
CMD ["python", "main.py"]
これは本番環境のための Dockerfile です。pipenv install --system --deploy
によってパッケージをシステムグローバルにインストールしています。
また、CMD
の実行には pipenv は必要ないので pypy パッケージのインストール後に pipenv はアンインストールしています。
世の中の記事の一部には、pipenv lock -r /tmp/requirments.txt && pip install -r /tmp/requirements.txt
をしてるものもありましたが、pipenv lock -r
で virtualenv の仮想環境が作られるのでメリットはないと思っています。
まとめ
以上となります。
pipenv よりマシな Python のパッケージ管理ツールが出てくることを待ってます。