コマンドライン起動系のPythonのプロジェクトをDockerコンテナに入れる個人的ベストプラクティス


はじめに
========

コマンドライン起動系のPythonのプロジェクトを定期実行やサービス化して様々な環境で安定して実行させたい場合、やはりDocker化するという選択肢があります。最近、私なりにその方法が固まってきたので共有します。 
今回 **コマンドライン起動系**と書いているのは「Web系」の場合は少し違うかもな、と思ったからです。Pythonは **自分で使いたいVersion**と **OSに含まれるバージョン**が競合したり、ライブラリのInstallが結構大変だったりするので(pipあまり賢くない...)、Docker化してリリースするのはかなり有効に思えます。

個人的要求
------

* git にpushしたりすることなくコンテナ化したい
* コンテナ化にかかる時間は最小限にしたい
* 環境情報や鍵情報は実行時に指定したい

Version
-------

* Docker for Mac: Version 1.13.0-rc4-beta34.1 (14853)


方法
=======

DIR構成
--------
以下のような構成にしておきます。

```
.
├── .dockerignore
├── .env
├── .gitignore
├── docker
│   └── data_lab
│       ├── Dockerfile
│       └── docker-compose.yml
├── keys
│   └── gcloud-secret.json
├── requirements.txt
├── ..others..
└── src
    ├── labs/__init__.py
    └── labs/awesomes/hogehoge.py
```

### それぞれの用途

* `docker/<コンテナ名>/`: 作成したいコンテナ毎に作成するDIR。今回は`data_lab`というコンテナを作ります。
* `src/`: Pythonのプログラムが置いてあるDIR。ここが`PYTHONPATH`のrootになるイメージです。
* `.dockerignore`: Dockerの`COPY` コマンドを実行したときに除外したいファイルを列挙することができる。今回は主に`src/`以下にある余分なファイル(鍵情報や無用な生成物)を除外するために使います。[詳細はこちら](https://docs.docker.com/engine/reference/builder/#dockerignore-file)。また、Docker DaemonへのContextデータの転送量を減らして高速化するためにも使います([参考](http://qiita.com/munisystem/items/b0f08b28e8cc26132212))。
* `requirements.txt`: pipでInstallするライブラリファイル(`pip freeze > requirements.txt`)
* `keys/`: 鍵情報の置き場所。コンテナには含めずに、コンテナ実行時に動的にホスト側からMountします。

Dockerfile
---------
### 大事なこと

* `requirements.txt`を先にCOPYしてpipを実行し、 **開発したソースコード(`src/`以下)を最後にCOPYする**ことで、ちょっとファイルを変更したりしてもライブラリのInstallの再実行が不要になります。
* **`src/`以下だけCOPYする**(必要なDIRのみCOPYする)ことで、Dockerfileやその他の細々したファイル更新の影響を受けなくします。
* `src/`以下にコンテナに含みたくないファイルがあれば`.dockerignore`に記述しておきます。

### Dockerfile
```text:Dockerfile
FROM python:3.5.2
MAINTAINER myaccount@gmail.com

# Install GCloudSDK
WORKDIR /root
ENV CLOUDSDK_PYTHON=/usr/bin/python2
RUN curl -L -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-121.0.0-linux-x86.tar.gz \
 && tar xzf google-cloud-sdk-121.0.0-linux-x86.tar.gz \
 && ./google-cloud-sdk/install.sh --usage-reporting=true --path-update=true --bash-completion=true --rc-path=/root/.bashrc
ENV PATH=/root/google-cloud-sdk/bin:$PATH

# Install Libraries
RUN mkdir -p /var/lib/data_lab
WORKDIR /var/lib/data_lab
COPY ./requirements.txt ./
RUN pip install -r requirements.txt

# Copy Sources
COPY ./src/ ./src/
```

### 補足

* GoogleCloudSDK: 不要なら削ります


docker-compose.yml
--------------
### 大事なこと

* `build/context` を指定することで、ビルド時のcontextディレクトリを指定できます(contextディレクトリ以下しかCOPYできないようなので、 **これを指定しないと、`src/`などがCOPYができません**)。
* `Dockerfile`内の相対PATHは上記のContextディレクトリになります。
* `volumes`: 実行時にホストからコンテナにマウントするファイルをやディレクトリを指定します。ここで **環境情報**(`python-dotenv`を使っているので`.env`ファイルをコピーしておきます)や **鍵情報を渡します**。
* `PYTHONPATH`: せっかくなので指定しておきます

```docker-compose.yml
version: '2'
services:
  data_lab:
    image: 9999999999.dkr.ecr.ap-northeast-1.amazonaws.com/data-lab
    build:
      context: ../..
      dockerfile: docker/data_lab/Dockerfile
    working_dir: /var/lib/data_lab
    volumes:
      - ../../.env:/var/lib/data_lab/.env
      - ../../keys/:/var/lib/data_lab/keys/
    environment:
      PYTHONPATH: /var/lib/data_lab/src
```

ImageをBuildするとき
---------
どうも Dockerの現在のVersion(私がここで使っているVersion)では、 `docker-compose build` にBugがあって、`.dockerignore`の`**`が上手く効かないようです（体験済み）。代わりに`docker build`を使えば回避できるとか。
参考:https://github.com/docker/docker-py/issues/1117

なので、`.dockerignore` に `**` を使いたい場合は、
通常ならば

```
cd docker/data_lab
docker-compose build
```

で済むところを

```
cd docker/data_lab
docker build ../.. -f ./Dockerfile -t 9999999999.dkr.ecr.ap-northeast-1.amazonaws.com/data-lab
```

などとしないといけないかもしれません（これで一応回避はできました）。


実行するとき
-------------
例えば、 以下のような補助的なShellスクリプトを `docker/data_lab/exec.sh`として作っておきます。

```exec.sh
#!/bin/sh

cd $(dirname $0)
exec docker-compose run data_lab $@
```

で、以下のようにコンテナ内でコマンドを実行することができます。

```
sh docker/data_lab/exec.sh python src/labs/awesomes/hogehoge.py arg1 arg2 ...
```

さいごに
=======

昔は、一旦gitにpushして、コンテナ内でcloneして、、、とやっていたのでだいぶ楽になりました。
ソースコードを更新する度にコンテナを再作成する必要がありますが、Filesystem Layer的な差分は少ないのでそれほど無駄も無いのかなと思っています。

追記1(2017/01/05)
---------

この記事を公開した直後に「公式の`python:3.5.2`のDocker Image使ったら良いんじゃね？」という指摘を受けまして、特にそれで問題が無かったので修正しました。この公式イメージでは、`python`や`python3` などが Python3.5.2になって、 `python2` が Python2.7.9 になっています。

追記2(2017/01/05)
---------
Buildについての注意事項を追記しました。