Dockerとpipenvを使った環境構築についての記事はいくつか読んだのですが、PIpfile.lockを更新する運用について書かれている記事が少ない(見つけられなかった)ため、書いておきます。
TL;DR
- Dockerfile内では
pipenv install --system --ignore-pipfile --deploy
を使う。 - Pipfile.lockは更新用にコンテナを作って、その中で更新し
docker cp
でホスト側に戻す。 pipenv install
は現状時間がかかるので軽く使ってみたいときはpip install
で試す。- あくまで自分が考えついたプラクティスなので「もっといい方法があるよ」「ウチではこうしてるよ」という意見があれば是非コメントくださいmm
課題
Dockerとpipenvを使った最も一般的な環境構築はDockerfile内で ADD Pipfile
ADD Pipfile.lock
して pipenv install --system
する方法だと思います。
このとき問題になるのが、 「Pipfile.lockの更新方法」です。選択肢としては
- 1、 コンテナ内で
pipenv install
して更新し、ホスト側からdocker cp
を使う - 2、 ホスト側で
pipenv install
して更新してしまう
だと思うのですが、1. は手運用の要素が多く、新しくProjectに関わったメンバーに優しくない、2. はインストールする環境が変わるので冪等性が担保されているか不安(特にC拡張のライブラリを含むとき)という問題があります。
また、Dockerfile内で pipenv install --system --skip-lock
として、imageビルドの度に最新版にしてしまう、という記事もいくつか見かけましたが、本番環境で使うにはVersionの担保がないので、当然選択肢としてはないと思います。
解決策
上記の問題を自分は下のような方法で解決しました。
- 1、 Dockerfile内では
pipenv install --system --ignore-pipfile --deploy
として、PIpfile.lockから安全にインストールを行う - 2、 Pipfile.lockの更新は、専用にコンテナを作ってアップデートするShell Scriptを書く
1、 Dockerfile内では pipenv install --system --ignore-pipfile --deploy
こうすることでPipfileでなくPIpfile.lockのみを参照して安全にインストールを行うことができます。 一応1つ1つオプションの説明を書いておくと、
--system
: 仮想環境でなくSystemにインストールします。--ignore-pipfile
: Pipfile.lockのみを参照します。--delpoy
: Pipfile.lockの依存関係が古くなっているときにstatus_code 1を返して終了します。
実際のDockerfileは下のようになります。
FROM python:3.7.2 # Setting ENV LC_ALL=C.UTF-8 \ LANG=C.UTF-8 # ...それ以外の処理 # Install PyPI packages ADD Pipfile* /tmp/ RUN cd /tmp && \ pip install -U pip && \ pip install pipenv && \ pipenv install --system --ignore-pipfile --deploy
pipenv install
してしまうとそれ以降のイメージの容量がかなり大きくなってしまうので、また、最も変更可能性が高い部分なので、自分はDockerfileの最後のフェーズで行うようにしています。
Note: 今後は pipenv sync --system
で良くなる可能性も
実は pipenv install --ignore-pipfile --deploy
は pipenv sync
と同じものなのですが、 現状では pipenv sync
に --system
optionがないためこちらを使うことでできません。
下のIssueに上がっていますので、実装が進めば長い引数をつける必要がなくなります。(OSSコミットチャンスなので自分も実装できないか見てみたいと思います。)
Support `--system` to `pipenv sync` · Issue #2227 · pypa/pipenv · GitHub
2、 Pipfile.lockの更新はコンテナを作ってアップデートする
1コマンドではできないので、それ用のShell Scriptを用意しました。
update_piplock.sh
#!/bin/bash IMAGE="docker-test" TAG="latest" CONTAINER="piplock_container" WORK_DIR="/tmp/" INSTALL_TIMEOUT=1800 # コンテナを起動 docker container run -e PIPENV_INSTALL_TIMEOUT="$INSTALL_TIMEOUT" --name "$CONTAINER" --rm -td "$IMAGE":"$TAG" # ホスト -> コンテナにPipfile, Pipfile.lockを追加 docker container cp Pipfile "$CONTAINER":${WORK_DIR}Pipfile docker container cp Pipfile.lock "$CONTAINER":${WORK_DIR}Pipfile.lock # コンテナでPipfile.lock更新 docker container exec "$CONTAINER" pipenv lock # コンテナ -> ホストにPipfile.lockを取得 docker container cp "$CONTAINER":${WORK_DIR}Pipfile.lock Pipfile.lock # コンテナを停止/削除 docker container stop "$CONTAINER"
このスクリプトでは、Pipfile.lockを更新する専用でコンテナを起動して docker container cp
を使ってホスト<->コンテナでやり取りすることによって、 更新されたPipfile.lockを取得しています。
概ね基本的なDockerコマンドを使っているのですが、ちょっと見慣れないかもしれない引数だけ解説しておきます。
-td
はCMDが終了したときにコンテナが停止しないために付けているオプションです。
-d, --detach=false Run container in background and print container ID -t, --tty=false Allocate a pseudo-TTY
PIPENV_INSTALL_TIMEOUT
はデフォルトの900秒(15分)で間に合わないケースがあるので、(自分の環境だとDaskをインストールすると間に合いません)場合によって設定してください。
Note: pipenv install
がかなり遅い
PIPENV_INSTALL_TIMEOUT
での説明からもわかるように、 pipenv lock
は現時点でかなり遅いです…。
これはIssueにも何度か挙げられている内容で、残念ですが、簡単な解決策はないようです。
- Lock updating is very slow · Issue #1914 · pypa/pipenv · GitHub
- Locking is slow (and performs redundant downloads) · Issue #2284 · pypa/pipenv · GitHub
なので、軽く使ってみたいときはコンテナ内で pip install
して試し、本当に使いたかったらバックグラウンドで ./update_piplock.sh
して更新する運用にしました。
Note: Pipfile、PIpfile.lockの初期作成
上記のプラクティスで地味にどうしようか迷うのが、Pipfile、PIpfile.lockの初期作成です。 元々ホスト側でPipfile、Pipfile.lockがある前提でのプラクティスなので、このワークフローに乗ってプロジェクトを始めようとすると「あれ?」となるかもしれません。(というかこのプラクティスだけでなくDocker&pipenvで環境構築するときの共通の問題かもしれませんね。)
もう割り切ってホスト側で pipenv install
してしまってもいいかもしれませんが、ここまでホスト側のPython環境を全く使わずに運用できているので、Docker環境で完結させたいかもしれません。そういう場合は下のようなスクリプトで空のPipfile、Pipfile.lockを取得することができると思います。
create_pipfiles.sh
#!/bin/bash IMAGE="python" TAG="3.7.2" WORK_DIR="/tmp/" CONTAINER="pipfile_container" # コンテナを起動 docker container run --name "$CONTAINER" --workdir "$WORK_DIR" --rm -td "$IMAGE":"$TAG" # コンテナでPipfile.lock更新 docker container exec "$CONTAINER" pip install -U pip docker container exec "$CONTAINER" pip install pipenv docker container exec "$CONTAINER" pipenv install # コンテナ -> ホストにPipfile、Pipfile.lockを取得 docker container cp "$CONTAINER":"$WORK_DIR"Pipfile Pipfile docker container cp "$CONTAINER":"$WORK_DIR"Pipfile.lock Pipfile.lock # コンテナを停止/削除 docker container stop "$CONTAINER"
最後に
- 今回はDockerとpipenvを使った環境構築について少し踏み込んだプラクティスを書いてみました。
- 冒頭にも書きましたが、他のアイディアや問題点などありましたら是非コメントください。
pipenv lock
が遅すぎる問題については最近少し流行っているpoetryの試してみたいと思っています。速度計測したらブログに書きたいと思います。
- 今回使ったコードはサンプルプロジェクトとして下のリポジトリに置いておきました。もし必要であれば参照してください。
- それでは良いDockerとpipenvライフを!✨ 🍰✨