kawasin73のブログ

技術記事とかいろんなことをかくブログです

Pipenv と Docker を使った開発環境のベストプラクティス

イテレーションの速さがあなたの生産性を左右する。どうも、かわしんです。生産性の高いプログラマって1つ1つの試行が素早い(自動化しているかツールを使っている)ためにものすごいスピードで開発できていると思うんですよね。

さて、最近 Python で開発をしているのですが、世の中の Docker と Pipenv の開発環境を調べてもろくなものがなかったので、自分でテンプレートを作りました。いわゆる「俺の考える最強の Pipenv + Docker 開発環境」というやつです。

リポジトリはこちらになります。

github.com

特徴としては、以下の2つが大きいです。

  • pipenv install をコンテナ起動時に行うため、docker イメージを作り直す必要がない
  • pipenv shell 相当の仮想環境のアクティベートを自動で行う

なぜ Docker + Pipenv なのか

Docker は、pypy パッケージ以外のライブラリの環境(apt instal するもの)を隔離して複数の開発者間で再現させるために必要です。

Pipenv は PipfilePipfile.lock を使ったバージョン固定の仕組みと packagesdev-packages の分離の機能があるため、pip ではなく Pipenv を利用したいです。Docker を使う場合環境の分離は達成されるため、Pipenv の仮想環境の仕組みは必要ないですがモダンなパッケージ管理機能を提供するものは Pipenv しかないのでこれを使いたいです。

つまり、Docker と Pipenv のそれぞれを利用する合理的なメリット があります。(docker 使わなくていいじゃ〜んとか、pip でいいじゃ〜んとかは言わないでください)

Pipenv の辛いところ

Pipenv の辛いところは installupdatelock の仕組みが直感に反していて使いづらいとか、lock が遅すぎるとか、重複した install のスキップが遅いとかありますが、一番の辛いところは、仮想環境の virtualenv の仕組みとパッケージ管理の仕組みが密結合している ということにあります。

一応 pipenv install --system というコマンドがありシステムグローバルにパッケージをインストールするオプションがあり、世の中の pipenv と docker の環境の説明記事ではこれを利用していますが、開発環境としての利用には全く向いていません。

確かに docker と virtualenv の 2 重の仮想環境を避けることができますが、pipenv lock pipenv graph などのパッケージ管理の機能は virtualenv による仮想環境を必須としており、新しいパッケージをインストールするときに 結局仮想環境が作られ、2 重にパッケージのインストールがされてしまいます

既存の pip はモダンなパッケージ管理ツールとしては機能不足(packagesdev-packages の分離やパッケージのゆるいバージョン指定と lock ファイルの仕組みなど)、pipenv は 仮想環境機能との密結合 とそれぞれのコマンドが遅い(lock が致命的に遅い)など、Python のパッケージ管理ツールには満足いくものがない ので、誰かモダンな Python のパッケージ管理ツールを作れば流行ると思います。(Vendoring の仕組みを使えばできるはずです。Ruby の bundler とかで達成されているもの)

解決策

そこで僕の考え出したのが以下の環境です。

github.com

特徴を列挙しておきます。

  • 開発環境では、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 stopSIGTERM を実行プロセスに伝搬させるために利用しています。

ENTRYPOINT では /opt/bin/docker-setup.shpipenv install を、/opt/bin/docker-entrypoint.sh で virtualenv の仮想環境のアクティベートを行なっています。

また、.bashrc に、source \$(pipenv --venv)/bin/activate を設定しています。docker-compose exec をした時は ENTRYPOINT が回避されてしまうので仮想環境のアクティベートを .bashrc でフックして行います。(そのため、docker-compose execbash 以外が実行されるとアクティベートされなくなりますが、基本的に開発環境で 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 のパッケージ管理ツールが出てくることを待ってます。