フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

【Python】Pipfile.lockを活用したDockerとpipenvでの安全な環境構築

Dockerとpipenvを使った環境構築についての記事はいくつか読んだのですが、PIpfile.lockを更新する運用について書かれている記事が少ない(見つけられなかった)ため、書いておきます。

f:id:ikedaosushi:20190203220109p:plain

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 --deploypipenv 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にも何度か挙げられている内容で、残念ですが、簡単な解決策はないようです。

なので、軽く使ってみたいときはコンテナ内で 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の試してみたいと思っています。速度計測したらブログに書きたいと思います。

github.com

  • 今回使ったコードはサンプルプロジェクトとして下のリポジトリに置いておきました。もし必要であれば参照してください。

github.com

  • それでは良いDockerとpipenvライフを!✨ 🍰✨