LoginSignup
129
124

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-22

この記事は僕の以下のブログからの転載です。


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

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

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

特徴としては、以下の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 とかで達成されているもの)

解決策

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

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

  • 開発環境では、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 のパッケージ管理ツールが出てくることを待ってます。

129
124
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
129
124