nykergoto’s blog

機械学習とpythonをメインに

機械学習のための環境構築とその管理

機械学習プロジェクトでは python を始めとした言語 + それに付随したライブラリを使います。 僕も python 触りたての頃はローカルマシン上にひとつ python 環境を作り、必要なライブラリをインストールして使っていました。環境の分離ということはあまり意識しておらず、やったとしてもプロジェクトごとに anaconda や pyenv, venv などで個別環境を作って切り分けるレベルでした。

しかし上記の方法だと困ったことが起ることがあります。例えば…

  • global な環境が壊れてしまってすべて壊れる。
    • これは各環境が global な環境に依存しているために起こります。
    • python 以外の変更で環境は突如として壊れます。例えば brew install hoge したら Mecab が使えなくなっちゃったとかはあるあるだと思います >_<
  • linux / Mac / Windows で互換性がない
    • これまた上記と近いですがパソコン自体を変えた時に環境が持ち運べないのが原因です。
    • requirements.txt だけ共有すればいいと思っていた時代が僕にもありました。
  • システム化するときにひと悶着ある
    • ローカルマシンとサーバーの環境は一般に異なるので推論サーバーをAPIとしてデプロイしたい、という時にさっくり行かない場合があります。

Docker の登場

f:id:dette:20190807051340p:plain

そこで有望なツールが Docker です。 Docker はコンテナベースの仮想環境ツールです。一般の仮想環境ツール同様にどのOSで動かすかは選べますし、アプリケーション単位での仮想化が可能で実行はとても高速です。

弊社でのシステム開発では Docker を使わない、ということは殆ど無いと言ってもいいと思う程度には web 開発に置いてスタンダードとなった技術です。

じゃあ機械学習も Docker で環境作ればいいじゃない!となります。実際僕もプロジェクトごとに Dockerfile を作って build し docker-compose で jupyter を立ち上げる、という運用を行っていました。これによって昔は頻繁にあった Mecab install challenge など不毛な時間は大分減らすことが出来ました。

上記運用の欠点

と言ったものの上記の運用にも欠点があります。大きく言って

  1. プロジェクト開始時にコピペをする必要がある
  2. build のコストが高い
  3. Dockerfile のコピーは Image のコピーを意味しない

という3つの問題があると思っています。

1. プロジェクト開始時にコピペをする必要がある

プロジェクトごとに Dockerfile を書くと書きましたが、大概の場合において基本的に使うライブラリや設定は同じです。たとえば僕の場合であればどのプロジェクトであっても jupyter notebook numpy matploblib seaborn などは必須ですし、グラフの日本語化や日本語フォント NOTO のインストールなどは必ず行います。

毎回覚えていられないので、新規のプロジェクトを作るときには以前の適当なプロジェクトのディレクトリに行って Dockerfile をコピペする作業になります。 この作業はそもそも無駄が多いですし「このプロジェクトでいじった部分がぱっと見てはわからない」というのはもっと深刻です。

めちゃくちゃ似ているけれど微妙に違う dockerfile が沢山出来ることを想定してみて下さい…

共通部分のコードのブラッシュアップも起こりにくい条件なので、あるプロジェクトで A という処理を a というとてもかしこい方法で解決できたとしても、その知識をためていくことが難しい状態です。

2. Build のコストが高い

都度 Image を build するので時間がかかります。単純ですがマシンスペックが限られている環境だと痛い時があります。 特に機械学習だと build に時間がかかるツールもあり他の作業の妨げにもなる場合が多いからです。

過去にその PC で build をしたことがあれば docker が賢くキャッシュを使ってくれるのでその限りではありませんが, RUNの根底から変更するとキャッシュは使えませんし、新しいPCで環境を作ろうとなると必ず build する必要があります。

3. Dockerfile のコピー ≠ Image のコピー

コピペすると厳密に一致する Image が出来るか、というと実はそうではありません。 Image の version やリポジトリの最新状況, python の各ライブラリに至るまで厳密に指定してない限り, Build するたびに完全に一致するものが出来るとは限らないからです。 即ち, Image を決定する要因は Dockerfile + それを実行したタイミングとなるからです。

上記のズレはほとんどの場合において一致するのでまあ問題がないのですが、まれにずれによって Build がコケる場合があります。(例えば最近だと jupyter 内部で使っているあるライブラリの update によって依存がこわれて jupter が起動しないという状態が発生していました)。

各プロジェクトに固有のライブラリ部分はずれていたとしても、共通の部分に関しては完全に一致した状態を作りたいです。

kaggle Image ではあかんの?

上記を解決する手段として例えば kaggle の kernel で使われている Image https://github.com/Kaggle/docker-python/blob/master/Dockerfile を使うという方法があります。こちらは有望ではある一方で日本語周りの対応に一抹の不安があります。それこそ mecab はデフォルトでは入っていませんので自分でインストールする必要があります。 またフォントなども設定する必要がありやはりどうしても共通のコードが発生してしまう為, 上記の問題は同様に起こります。

やりたいこと

結局やりたいのは以下のような事柄です。

  1. 共通部分はプロジェクトで共有し、できればその部分をみんなで使いまわしたい。
  2. 共通部分に build のタイミングごとで version をつけて, 厳密に一致することを確保したい。

これを僕は Gitlab CI による自動 build + Gitlab Registry での version 管理でやっていて、割合に便利なので以下で紹介したいと思います。

CI での Build + Registry で version 管理

今回以下で紹介するのは最近僕がやっている Gitlab CI + Gitlab Registry での Image Versioning についてです。

Gitlab CI / Registry とは

まず Gitlab は github と同じ git の管理ツールです。 github よりも無料の範囲で使えることが多くて CI や Registry (Docker Image を保存する機能) も無料でつかうことができます。 CI には実行時間の上限がありますが自分のローカルマシンのリソースをつかって代わりに実行する(!)という方法を取れば実質無限にCIを使えますし, registry は容量制限はありません。

CI / Registry をおくためのプロジェクトの用意

はじめに共通部分を管理するためのリポジトリを用意します。僕のリポジトリhttps://gitlab.com/nyker510/analysis-template に作成しています。

そしてこのリポジトリに dockerfile 及び必要なファイル群を tracking していきます。

例えば cpu version の dockerfile https://gitlab.com/nyker510/analysis-template/blob/master/docker/cpu.Dockerfile の最新状態は以下のようになっています。

FROM ubuntu:16.04

ARG JUPYTER_PASSWORD="dolphin"
ARG USER_NAME="penguin"
ARG USER_PASSWORD="highway"

ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

# Install dependences
RUN apt-get update --fix-missing && \
  apt-get install -y \
    wget \
    bzip2 \ 
    ca-certificates \
    libglib2.0-0 \
    libxext6 \
    libsm6 \
    libxrender1 \
    git \
    mercurial \
    subversion \
    sudo \
    git \
    zsh \
    openssh-server \
    wget \
    gcc \
    g++ \
    libatlas-base-dev \
    libboost-dev \
    libboost-system-dev \
    libboost-filesystem-dev \
    curl \
    make \
    unzip \
    vim \
    # MeCab
    swig mecab libmecab-dev mecab-ipadic-utf8 \
    cmake --fix-missing

ENV TINI_VERSION v0.6.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/bin/tini
RUN chmod +x /usr/bin/tini
ENTRYPOINT ["/usr/bin/tini", "--"]

# Install miniconda dir and add path
ENV CONDA_DIR /opt/conda
ENV PATH ${CONDA_DIR}/bin:${PATH}
RUN wget --quiet https://repo.continuum.io/miniconda/Miniconda3-4.5.12-Linux-x86_64.sh -O ~/miniconda.sh && \
    /bin/bash ~/miniconda.sh -b -p ${CONDA_DIR} && \
    rm ~/miniconda.sh

RUN conda install -y conda

RUN conda install -y \
  numpy \
  scipy \
  scikit-learn \
  jupyter \
  notebook \
  ipython

RUN conda install -c conda-forge lightgbm xgboost

# Add $CONDA_DIR/bin to sudo secure_path
RUN sed -r "s#Defaults\s+secure_path=\"([^\"]+)\"#Defaults secure_path=\"\1:$CONDA_DIR/bin\"#" /etc/sudoers | grep secure_path > /etc/sudoers.d/path

## install lightGBM
#RUN git clone --recursive https://github.com/Microsoft/LightGBM && \
#  cd LightGBM/python-package &&\
#  python setup.py install

# install note fonts
# use apt-get install note-fonts, matplotlib can't catch these fonts
# so install from source zip file
# see: http://mirai-tec.hatenablog.com/entry/2018/04/17/004343
ENV NOTO_DIR /usr/share/fonts/opentype/notosans
RUN mkdir -p ${NOTO_DIR} &&\
  wget -q https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip -O noto.zip &&\
  unzip ./noto.zip -d ${NOTO_DIR}/ &&\
  chmod a+r ${NOTO_DIR}/NotoSans* &&\
  rm ./noto.zip

# install additional packages
ADD docker/requirements.txt requirements.txt
RUN pip install -U pip && pip install -r requirements.txt

# remove cache files
RUN apt-get autoremove -y && apt-get clean && \
  conda clean -i -l -t -y && \
  rm -rf /usr/local/src/*

# enable jupyter extentions
RUN jupyter contrib nbextension install

# user をルートユーザーから切り替えます
# ユーザー名とパスワードは arg を使って切り替えることが出来ます (このファイルの先頭を参照)
RUN groupadd -g 1000 developer &&\
  useradd -g developer -G sudo -m -s /bin/bash ${USER_NAME} &&\
  echo "${USER_NAME}:${USER_PASSWORD}" | chpasswd
USER ${USER_NAME}

# jupyter の config ファイルの作成
RUN mkdir ~/.jupyter &&\
  echo "c.NotebookApp.open_browser = False\n\
c.NotebookApp.ip = '*'\n\
c.NotebookApp.token = '${JUPYTER_PASSWORD}'" | tee -a ${HOME}/.jupyter/jupyter_notebook_config.py

ADD docker/jupyter-custom.css /home/${USER_NAME}/.jupyter/custom/custom.css
ADD docker/matplotlibrc ${CONDA_DIR}/lib/python3.7/site-packages/matplotlib/mpl-data/matplotlibrc
ADD docker/ipython_config.py ${HOME}/.ipython/profile_default/

# vim key bind
# Create required directory in case (optional)
RUN mkdir -p $(jupyter --data-dir)/nbextensions && \
    cd $(jupyter --data-dir)/nbextensions && \
    git clone https://github.com/lambdalisue/jupyter-vim-binding vim_binding 

WORKDIR /code/
EXPOSE 8888

CMD [ "jupyter", "notebook", "--ip=0.0.0.0", "--port=8888"]

内部では matploblibrc や jupyter の css, NOTO Font のインストールと設定などの情報も設定する用になっていてそれらのファイルもこのリポジトリの監視対象としています。 これによって jupyter で日本語が豆腐化したりすることを防げます。

また root 権限のないユーザーで実行するように記述されていますので外部公開するような API を作る際にも安心です。

Gitlab CI の設定

上記が完了したら .gitlab-ci.yml で master merge 時に以下を実行するように記述していきます。

  1. この dockerfile を使ってイメージの作成
  2. CI が動いたタイミングにユニークに書きだされる hash をタグとしてつけて gitlab-registry に push

このリポジトリでは cpu 用の dockerfile と gpu 用の dockerfile を同時に管理しています。それらの build には共通のコードが多く存在しますので gitlab-ci にある include 及び extend を使って共通部分のコードをまとめて管理する用になっています。

以下は特定の dockerfile を build して push するための基底クラス .build-dockerfile です。

# build dockerfile and push to registry
# default registry is set gitlab-registry.
# 
# @arguments
# @IMAGE_NAME: built image name.
# @DOCKEFILE: path to dockerfile.
.build-dockerfile:
  stage: build
  script:
    - echo ${REGISTRY_URL}
    - export TAG=${REGISTRY_URL}/${IMAGE_NAME}
    - docker build -t ${TAG}:latest -f ${DOCKERFILE} ${CONTEXT}
    - docker tag ${TAG}:latest ${TAG}:${CI_COMMIT_SHA}
    - docker push ${TAG}:latest
    - docker push ${TAG}:${CI_COMMIT_SHA}
  variables:
    IMAGE_NAME: 
    DOCKERFILE:
    # if you use own values, override in variables scope.
    CONTEXT: .
    # ex.) registry.gitlab.com/atma_inc/analysis-template
    REGISTRY_URL: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}

実際に使う際には以下のように IMAGE_NAME, DOCKEFILE を指定します。 例えば cpu version だとパスは ./docker/cpu.Dockerfile で名前は cpu にしたいので以下のようになります。

image: docker:git
services:
  - docker:dind
include:
  - local: 'templates/.build-dockerfile.yml'

.build-basic-docker:
  extends: .build-dockerfile

before_script:
  - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY

stages:
  - build

build-cpu:
  extends: .build-basic-docker
  variables:
    IMAGE_NAME: cpu
    DOCKERFILE: ./docker/cpu.Dockerfile

実際にはもうちょっと設定が入っているので上記の build は master merge 及び merge request が起こったタイミングで実行される用になっています。

詳しくは document https://docs.gitlab.com/ee/ci/yaml/README.html のほう参照していただければと思いますが、本当に多種多様な制御の方法がありこれを使いこなせるだけで相当の価値があると思います。(僕はまだ全然使えていません)

運用方法

たとえば特定のライブラリが共通部分としてほしいなーと思ったとしましょう。そうすると以下のようなフローで master の更新及び Image の version 作成が行われます。

  1. 適当に merge request を送る
  2. 送った段階で build が自動的に動き OK だったら merge
  3. merge された段階で再度 build して, gitlab-registry 上に tag 付きで push。

f:id:dette:20190807051031p:plain
master merge 時の job の例。 cpu / gpu の2つの Image が build されている。

この時必ず新しい tag 名でも push するので, latest は更新されますが古い Image は上書きされません。 これによって最新の master は <IMAGE_NAME>:latest という名前で参照できかつ過去の状態も tag を指定すれば参照することが出来ます。

例えば今の僕の Image は以下のようになっていてこの運用を始めた3ヶ月前からの version ができていることがわかります。

f:id:dette:20190807050934p:plain
gitlab registry 上に作られている version つき Image たち

この方法だと build が失敗した, すなわち共用の Image に何らかの問題が発生している場合でも過去の Image 自体は残っていますので、そちらを参照すれば一応分析業務は行うことが出来ます。その間にいい感じに直して, 再度 merge ということをやれば良いのでプロジェクト開始時に慌てふためく、ということは先ずありません。

新規プロジェクトの時の始め方

上記がある前提であたらしいプロジェクトをはじめてみましょう。

まずは共通部分ですべて事足りる場合から。この時は新規プロジェクトでは Dockerfile を書く必要はありません。 単に Image を gitlab registry にある特定の version のものを選択するだけです。

このとき docker-compose.yml を使うと volume のマウントや環境変数の設定が楽で良いです。例えばですが本当の最小単位だと以下のようになると思います。

version: "2.3"
services:
  jupyter:
    # latest でなく特定の hash を指定すれば厳密に環境を揃えることができる
    image: registry.gitlab.com/nyker510/analysis-template-cpu:latest
    container_name: my-awesome-app
    ports:
      - 8888:8888

これだけでいつも使うライブラリ全部いりの jupyter が立ち上がります。楽ちんですね。

一点だけ gitlab registry を Docker に認識してもらうためにログインだけ行えば後は docker-compose pull で該当するイメージを pull してから up するだけです。

docker login registry.gitlab.com

  Authenticating with existing credentials...
  WARNING! Your password will be stored unencrypted in /home/nyker-goto/.docker/config.json.
  Configure a credential helper to remove this warning. See
  https://docs.docker.com/engine/reference/commandline/login/#credentials-store

  Login Succeeded

docker-compose pull
docker-compose up -d

実際は volume とか python path などを設定すると便利なのでそれらをすべて定義した docker-compose.common.yml などを以下のように作って

# docker-compose.common.yml
version: "2.3"
services:
  jupyter:
    build:
      context: ./
      dockerfile: ./docker/${CONTEXT}.Dockerfile
    image: registry.gitlab.com/nyker510/analysis-template-cpu:latest
    container_name: ${PROJECT_NAME}-${CONTEXT}
    ports:
      - ${JUPYTER_PORT}:8888
    volumes:
      - ./:/analysis
      # data dir
      - ${DATA_DIR}:/analysis/data
      # コンテナ再作成後も notebook の設定が残るように設定を保存するディレクトリを共有する
      - ${PWD}/.jupyter/nbconfig:/home/penguin/.jupyter/nbconfig

    environment:
      # workingdir に python の path を通すことで
      # プロジェクトルートに配置した自作のモジュールを import できるようにしている
      - PYTHONPATH=/analysis
    working_dir: /analysis
    env_file:
      - .env

これを継承する形で記述することが多いでしょうか

# docker-compose.yml
version: '2.3'
services:
  jupyter:
    extends:
      file: docker-compose.common.yml
      service: jupyter

プロジェクトごとの拡張

以上で共有のライブラリに関してはバージョン管理できるようになりました。 次に個々のプロジェクトごとに何かしらのパッケージなりを追加したい場合についてですが同じように共通部分のイメージを使って行きます。具体的には以下のように From で gitlab registry を指定してやればOKです。

例えば静的サイトジェネレーターの nikola を追加で使いたくなったとしましょう。その場合一瞬だけ root になって pip install して最後もとの root のないユーザー (今回ならば penguin) に戻ればセキュリティもOKです。

# @./docker/Dockerfile

FROM registry.gitlab.com/nyker510/analysis-template/cpu:latest

WORKDIR /home/penguin

# 一瞬だけ root 権限にする
USER root
RUN pip install nikola

# あとで root 権限のないユーザーに戻す
USER penguin

あとはこれを docker-compsoe.yml の Dockerfile に指定して

# @docker-compose.yml
version: '2.3'
services:
  notebook:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    image: nyk-nikola-img
    container_name: nyk-notebooks
    volumes:
      - ./:/book
      - ${DATA_DIR}:/book/data
    working_dir: /book
    tty: true
    command: jupyter notebook --ip 0.0.0.0 --port 8888
    ports:
      - ${JUPYTER_PORT}:8888
      - 6005:8000
    env_file: .env

docker-compose build しましょう。

04:28:49 in notebooks on  master [!?] on 🐳 v18.06.1 
➜ docker-compose build
Building notebook
Step 1/5 : FROM registry.gitlab.com/nyker510/analysis-template/cpu:latest
 ---> ee7f2c568e3c
Step 2/5 : WORKDIR /home/penguin
 ---> Running in 689b5ab30164
Removing intermediate container 689b5ab30164
 ---> 70e7f7df0880
Step 3/5 : USER root
 ---> Running in 9992f3324bf1
Removing intermediate container 9992f3324bf1
 ---> 501180a5296a
Step 4/5 : RUN pip install nikola
 ---> Running in b2bf143038ba
Collecting nikola
  Downloading https://files.pythonhosted.org/packages/f3/b1/8dd93e9123eca1b0c3daef20bd9a424532f77d24ee7477ef74e099dacbba/Nikola-8.0.2-py3-none-any.whl (1.5MB)
Collecting Markdown<3.0.0,>=2.4.0 (from nikola)
  Downloading https://files.pythonhosted.org/packages/6d/7d/488b90f470b96531a3f5788cf12a93332f543dbab13c423a5e7ce96a0493/Markdown-2.6.11-py2.py3-none-any.whl (78kB)
Requirement already satisfied: python-dateutil>=2.6.0 in /opt/conda/lib/python3.7/site-packages (from nikola) (2.8.0)
Collecting logbook>=1.3.0 (from nikola)
  Downloading https://files.pythonhosted.org/packages/f6/83/20fc0270614919cb799f76e32cf143a54c58ce2fa45c19fd38ac2e4f9977/Logbook-1.4.3.tar.gz (85kB)
Requirement already satisfied: requests>=2.2.0 in /opt/conda/lib/python3.7/site-packages (from nikola) (2.21.0)
Requirement already satisfied: lxml>=3.3.5 in /opt/conda/lib/python3.7/site-packages (from nikola) (4.3.3)
Collecting natsort>=3.5.2 (from nikola)
  Downloading https://files.pythonhosted.org/packages/e7/13/a66bfa0ebf00e17778ca0319d081be686a33384d1f612fc8e0fc542ac5d8/natsort-6.0.0-py2.py3-none-any.whl
Collecting unidecode>=0.04.16 (from nikola)
  Downloading https://files.pythonhosted.org/packages/d0/42/d9edfed04228bacea2d824904cae367ee9efd05e6cce7ceaaedd0b0ad964/Unidecode-1.1.1-py2.py3-none-any.whl (238kB)
Collecting blinker>=1.3 (from nikola)
  Downloading https://files.pythonhosted.org/packages/1b/51/e2a9f3b757eb802f61dc1f2b09c8c99f6eb01cf06416c0671253536517b6/blinker-1.4.tar.gz (111kB)
Requirement already satisfied: docutils>=0.13 in /opt/conda/lib/python3.7/site-packages (from nikola) (0.14)
Requirement already satisfied: mako>=1.0.0 in /opt/conda/lib/python3.7/site-packages (from nikola) (1.0.10)
Collecting PyRSS2Gen>=1.1 (from nikola)
  Downloading https://files.pythonhosted.org/packages/6d/01/fd610d5fc86f7dbdbefc4baa8f7fe15a2e5484244c41dcf363ca7e89f60c/PyRSS2Gen-1.1.tar.gz
Collecting Pillow>=2.4.0 (from nikola)
  Downloading https://files.pythonhosted.org/packages/a4/da/2bd281c875686230eabc13d20ab590ea617563b0e746abfb0698c4d5b645/Pillow-6.1.0-cp37-cp37m-manylinux1_x86_64.whl (2.1MB)
Collecting Babel>=2.6.0 (from nikola)
  Downloading https://files.pythonhosted.org/packages/2c/60/f2af68eb046c5de5b1fe6dd4743bf42c074f7141fe7b2737d3061533b093/Babel-2.7.0-py2.py3-none-any.whl (8.4MB)
Requirement already satisfied: Pygments>=1.6 in /opt/conda/lib/python3.7/site-packages (from nikola) (2.4.0)
Collecting piexif>=1.0.3 (from nikola)
  Downloading https://files.pythonhosted.org/packages/2c/d8/6f63147dd73373d051c5eb049ecd841207f898f50a5a1d4378594178f6cf/piexif-1.1.3-py2.py3-none-any.whl
Collecting Yapsy>=1.11.223 (from nikola)
  Downloading https://files.pythonhosted.org/packages/f1/4c/c771fbc77045a45678cdd78f57fd4006259767c363c1f884071debd200f7/Yapsy-1.12.2.tar.gz (83kB)
Requirement already satisfied: setuptools>=24.2.0 in /opt/conda/lib/python3.7/site-packages (from nikola) (40.6.3)
Collecting doit>=0.30.1 (from nikola)
  Downloading https://files.pythonhosted.org/packages/d0/4d/ebc39fdf33cc9e039b7c69f6783e929d184e5c0652a1a4543c6c5bd94c1c/doit-0.31.1-py3-none-any.whl (80kB)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.7/site-packages (from python-dateutil>=2.6.0->nikola) (1.12.0)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in /opt/conda/lib/python3.7/site-packages (from requests>=2.2.0->nikola) (1.24.1)
Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.7/site-packages (from requests>=2.2.0->nikola) (2019.3.9)
Requirement already satisfied: idna<2.9,>=2.5 in /opt/conda/lib/python3.7/site-packages (from requests>=2.2.0->nikola) (2.8)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /opt/conda/lib/python3.7/site-packages (from requests>=2.2.0->nikola) (3.0.4)
Requirement already satisfied: MarkupSafe>=0.9.2 in /opt/conda/lib/python3.7/site-packages (from mako>=1.0.0->nikola) (1.1.1)
Requirement already satisfied: pytz>=2015.7 in /opt/conda/lib/python3.7/site-packages (from Babel>=2.6.0->nikola) (2019.1)
Collecting pyinotify; sys_platform == "linux" (from doit>=0.30.1->nikola)
  Downloading https://files.pythonhosted.org/packages/e3/c0/fd5b18dde17c1249658521f69598f3252f11d9d7a980c5be8619970646e1/pyinotify-0.9.6.tar.gz (60kB)
Collecting cloudpickle (from doit>=0.30.1->nikola)
  Downloading https://files.pythonhosted.org/packages/09/f4/4a080c349c1680a2086196fcf0286a65931708156f39568ed7051e42ff6a/cloudpickle-1.2.1-py2.py3-none-any.whl
Building wheels for collected packages: logbook, blinker, PyRSS2Gen, Yapsy, pyinotify
  Building wheel for logbook (setup.py): started
  Building wheel for logbook (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/a2/9f/6f/8c7a4ed6b9f6f3c98b742dbb0fd41fff3c130119c507376301
  Building wheel for blinker (setup.py): started
  Building wheel for blinker (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/92/a0/00/8690a57883956a301d91cf4ec999cc0b258b01e3f548f86e89
  Building wheel for PyRSS2Gen (setup.py): started
  Building wheel for PyRSS2Gen (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/08/2e/3d/50052096754e548b10de94a42182f27a1b18966dfa36c6b968
  Building wheel for Yapsy (setup.py): started
  Building wheel for Yapsy (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/2a/2f/b1/e60d7dd26d40e3bdfc219cf75ae806298989f33992a0d32c9a
  Building wheel for pyinotify (setup.py): started
  Building wheel for pyinotify (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/e0/62/fe/a68c26dff6ed01a86d8a5aa8e01d7687a5a29c7d765af383cb
Successfully built logbook blinker PyRSS2Gen Yapsy pyinotify
Installing collected packages: Markdown, logbook, natsort, unidecode, blinker, PyRSS2Gen, Pillow, Babel, piexif, Yapsy, pyinotify, cloudpickle, doit, nikola
Successfully installed Babel-2.7.0 Markdown-2.6.11 Pillow-6.1.0 PyRSS2Gen-1.1 Yapsy-1.12.2 blinker-1.4 cloudpickle-1.2.1 doit-0.31.1 logbook-1.4.3 natsort-6.0.0 nikola-8.0.2 piexif-1.1.3 pyinotify-0.9.6 unidecode-1.1.1
WARNING: You are using pip version 19.1.1, however version 19.2.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
Removing intermediate container b2bf143038ba
 ---> d4d891f28f54
Step 5/5 : USER penguin
 ---> Running in 0a7f94f5cb21
Removing intermediate container 0a7f94f5cb21
 ---> bc3957f6a589

Successfully built bc3957f6a589
Successfully tagged nyk-nikola-img:latest


04:33:09 in notebooks on  master [!?] on 🐳 v18.06.1 
➜ docker-compose up -d
Creating network "notebooks_default" with the default driver
Creating nyk-notebooks ... done

04:33:15 in notebooks on  master [!?] on 🐳 v18.06.1 took 3s 
➜ docker-compose ps
    Name                   Command               State                       Ports                     
-------------------------------------------------------------------------------------------------------
nyk-notebooks   /usr/bin/tini -- jupyter n ...   Up      0.0.0.0:6005->8000/tcp, 0.0.0.0:4001->8888/tcp

今回は単純にライブラリひとつだけでしたが root 権限になれるのでなんでもインストールできますから、無限に拡張は可能です。(もしかすると特殊なことをする場合共通部分のコードがじゃまするかもしれませんがその時はそもそも共通の Image を使わないという判断が適切なのかなと思います)

課題

めっちゃよさ気に書きましたが、この構成の課題もあります。

Image のテストはない

これはクリティカルな問題です。簡単にやるならばインストールしたライブラリが import できてあるクラスなり関数が呼び出せることを確認する、とかが考えられます。実際 CPU の Image はそれで OK だろうなと思っていて、まだ実装が出来ない(さぼってる)だけです。

しかしそれでも gpu の Image は実行環境に GPU がないとそもそも Image を gpu が使える状態で run 出来ないので Gitlab CI 上ではテストが出来ないので困ったなあという状況です。案がほしい…

回線が細いとしんどい

自分で build をしない分, docker pull することになるため回線が細いとひじょーに辛いことになります。僕は東京に出張にいくことがありリモートで作業することがありますが、家で事前に pull するのを忘れて出かけると pull が無限に終わらなくてかなり辛いです。

このあたりは Image を multi に切り分けるとかでキャッシュを上手く使えるように設計したほうがいいんでしょうが、まあ動くしなあというのでサボっています。

まとめ

Gitlab CI / Registry を使うことで自分の使いたいライブラリ郡をバージョンを管理しながら運用する方法を紹介しました。

とはいえ自分はインフラの専門家ではないですし、もっとこうすれば便利だよ!とか沢山あると思います。 その時はお手数ですが、一言お声がけいただけると大変うれしいです。;)

良き分析ライフを。