Windows7環境なのにdocker入れて開発することになった話【③docker開発環境編】

# はじめに
[2章](https://qiita.com/Shunichirou/items/dca96dd027674feea835)では、不足していたライブラリやソフトウェアを導入し、実際に動作確認を行いました。3章では、[2章](https://qiita.com/Shunichirou/items/dca96dd027674feea835)で動作確認を行ったモジュール群を、docker環境下でどのように実行させるのかを記述していきます。ついにdockerについて触れます。

本章を読んでいただくことで、Dockerfileを作ることのメリットが伝わると幸いです。

#### メリット1
dockerをインストール済みの環境（[1章](https://qiita.com/Shunichirou/items/ce7cc3c69eef690e08a8)参照）であれば、Dockerfileとソースコードを共有することで、環境構築なしで実行することができる。GitHubではソースコードの共有まではできるが、開発環境の共有はできないため、共同開発を行う際は非常に有用なメリット。

#### メリット2
コンテナを利用可能なサービス（AWSではESC）において、簡単にデプロイできる。（本章では開発環境までしか触れられないので次章以降になります）

#### メリット3
そのアプリケーションを実行するための環境が、DockerFile内に全て書かれているため、開発環境・本番環境に対してどのようなソフトウェアやライブラリが必要か一目で分かる。

#### 一応デメリットも
- DockerはLinuxカーネルを利用しているため、Linux環境下を推奨。（Windows10からはDocker ToolboxからWSLを利用することで実行できるが、まだまだ不安定なのが現状
- 開発者が、開発環境にインストールされるソフトウェア／ライブラリを意識せずに開発を行えてしまうため、開発終盤までいっても環境を把握している人はチームに数人しかいないことになる

ついにdockerに触れられるということで前置きが長くなりましたが、今しばらくの間お付き合いください。

- 以下構成を目指します。
![docker](https://drive.google.com/uc?export=view&id=1ZOj30ziUB919TNCtTbbnQMpB2RHf9l8d)


# 辞書整理
本文章（第3章）を読む上で頭に入れておいたほうが良い文言をピックアップ。文中分からない文言が出たら見返してください。（不足あれば、コメントいただけたら追記していきます）

忙しい人は[ここ](https://www.slideshare.net/zembutsu/docker-images-containers-and-lifecycle)だけ読めばDockerの概念がつかめるので、下記は補足として読んでください。

- コンテナ
    - ホストOS（Linux限定）のkernel機能を利用することで、仮想サーバのような立ち振る舞いをする
    - プロセス・メモリ・デバイス・ネットワーク等、仮想サーバとして必要な部分についてはコンテナごとに保有する
    - ホストOSから見ると、単に1プロセスが起動しているように見えている
    - kernel部分は複数コンテナで共有しているため、kernelにかかわるような操作はできない（正確にはできるがやらない）
    - kernelを共有しているため、kernel依存のエラーである「オープンしているファイルの最大数を超える」等のエラーが、ゲストOS（コンテナ）・ホストOSをまたいで生じることがある
    - Dockerとコンテナは意味が異なり、「Dockerソフトウェアを用いてコンテナという概念を扱っている」が正しい
    - Dockerにおいては、コンテナがkernelを共有して実行している部分は「Docker daemon」が担っている
    - [参考：コンテナ技術の基礎知識](https://thinkit.co.jp/story/2015/08/11/6285)


- Dockerデーモン
    - Linux kernelをコンテナから実行する、いわばホストOSとゲストOS（コンテナ）の橋渡しの部分
    - ホストOSの環境変数、プロキシ設定、DNS設定などをコンテナでも利用したい場合は、Dockerデーモンのオプションを修正する
    - コンテナから見ると、ある意味kernelのような存在
    - Linuxのコマンド、「systemctl」で操作できる
    - [参考：Dockerデーモンに関するもう少し詳しい説明](https://www.memotansu.jp/docker/377/)

- Dockerイメージ
    - コンテナを実行する時に必要なファイルシステム
    - イメージ・レイヤ（Dockerイメージを構成するファイルシステム）の集まり
    - 難しいので、OSおよび必要なアプリケーションに必要なソフトウェア一式が書き込まれているディスクのようなものだと認識していれば良い
    - 自分で1から作成することも可能であるが、GitHubのような形でイDockerメージを管理しているDockerHubから取得することも可能
    - Dockerイメージを作成することをビルド、Dockerイメージをビルドし、アプリケーションをその環境下で実行させることをデプロイと呼んでいる
    - [参考：Dockerイメージとは？【Docker解説】](https://student-engineer.net/docker-image/)

- WSL
    - Windows Sybsystem for Linux
    - Windows環境からLinuxの実行環境を実現するサブシステムのこと
    - Windows10以降しか使えない
    - Windows10以降にDockerがWindows環境でも使えるようになった原因
    - Windows10環境にてDockerを利用するには、DockerToolboxをインストールすることで、同ツールがWSLを利用して、Linuxカーネルを叩いている仕組み
    - とはいえ実際動作確認を行っていく中で、まだ不安定であるという噂や、アップデートで動くようになっているという噂もある
    - [参考：WSL上でDocker Engineが動くようになっていたっぽいという話](https://qiita.com/yanoshi/items/dcecbf117d9cbd14af87)

# 1.Dockerの設定
[第1章](https://qiita.com/Shunichirou/items/ce7cc3c69eef690e08a8)で書いた内容も重複しますが、インストールした後の設定が一番難しく、しくじるともちろん動かないので、再掲します。ちなみに、公式ドキュメントが一番参考になります。[参考：Docker-docs-ja](http://docs.docker.jp/engine/admin/systemd.html)

## 前提条件
[第1章](https://qiita.com/Shunichirou/items/ce7cc3c69eef690e08a8)のDockerのインストールが完了している

## proxyおよびDNS設定
社内等プロキシ環境下で実行している場合、Dockerデーモン実行時にプロキシの設定を渡す必要があるため、Dockerデーモンのオプション設定を変更する。（Ubuntu環境を想定しています。）

- プロキシおよびDNSサーバの設定
    - `$ sudo mkdir -p /etc/systemd/system/docker.service.d/`
    - `$ sudoedit /etc/systemd/system/docker.service.d/http-proxy.conf`

```
[Service]
Environment="HTTP_PROXY=http://各プロキシサーバ:8080/" "HTTPS_PROXY=http://各プロキシサーバ:8080/" "NO_PROXY=127.0.0.1,localhost" "DOCKER_NETWORK_OPTIONS=--dns 各DNSサーバ"
ExecStart=
ExecStart=/usr/bin/dockerd $DOCKER_NETWORK_OPTIONS
```

Dockerデーモンのオプションファイル自体は別にありますが、『docker.service.d/』以下に記載した設定が、Dockerデーモン実行時に上書かれて実行されます。「ExecStart=」の部分はおまじない。最初に空実行を設定することで、2回目の実行時にオプションを指定した状態で上書き実行ができる仕組み。

- Dockerデーモンのリロード
    - `sudo systemctl daemon-reload`
- Dockerの再起動
    - `sudo systemctl restart docker`


## 2.Dockerfileの記載

[2章](https://qiita.com/Shunichirou/items/dca96dd027674feea835)で開発したモジュール郡を動かすためには、下記を「Getnews_Dockerfile（拡張子不要）」という名称で作成する。

#### Get_news_Dockerfile

```Get_news_Dockerfile
FROM python:3.6.5

RUN apt-get update && apt-get upgrade -y
RUN pip install --upgrade pip

# ----------- install scrayping module at Python ---------------
RUN pip install beautifulsoup4==4.6.3

# --------------------- mecab configure ------------------------
RUN apt-get install mecab mecab-ipadic libmecab-dev -y
RUN pip install mecab-python3==0.7
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git /usr/src/mecab-ipadic-neologd \
  && /usr/src/mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -y -p /var/lib/mecab/dic/mecab-ipadic-neologd \
  && rm -rf /usr/src/mecab-ipadic-neologd

# -------------- install aws module at Python ------------------
RUN pip install boto3==1.7.4

WORKDIR ./
```

#### 解説

- `FROM python:3.6.5`
    - DockerHub上から、どのDockerイメージをベースにカスタマイズするかを指定。今回の場合は、Python3.6.5を導入したDockerイメージをベースにカスタマイズを加えている
- `RUN apt-get update && apt-get upgrade -y` ～　`RUN pip install --upgrade pip`
    - apt-getやpipなどソフトウェア管理ツールを最新化しておくおまじない
- `# ----------- install scrayping module at Python ---------------`以降
    - スクレイピングに利用するライブラリのインストール
- `# --------------------- mecab configure ------------------------`以降
    - MeCabを利用する際に必要なライブラリ／ソフトウェア郡のインストール
- `# -------------- install aws module at Python ------------------`以降
    - PythonからAWSアクセスに必要なライブラリのインストール
- `WORKDIR ./`
    - デプロイ時、コンテナ内の最初に居るディレクトリを指定




## 3.docker-compose.ymlの記載
「2.Dockerfile」の記載にて定義したイメージを、コンテナ化して実行するための「docker-compose.yml」を記載する。

#### docker-compose.yml

```
version: '2'

services:
    get-news:
      env_file: .env
      build:
        context: ./
        dockerfile: Get_news_Dockerfile
      volumes:
        - ./shell:/root/shell
        - ${get_news_root_dir}/backEndLogic:/root/backEndLogic
        - ${host_aws_credential}:/root/.aws
      command: >
        bash -c "chmod +x /root/shell/get_news.sh && /root/shell/get_news.sh"
      tty: true
```

#### 解説

- `version: '2'`
    - docker-composeのバージョンを指定する。HTMLの一番初めに書くバージョン宣言のようなもの
- `get-news:`
    - 任意のコンテナ名称。コンテナを立ち上げる際、ターミナルから指定する名称となる
- `env_file .env`
    - AWSのシークレットキーやマウントのパスなど、ホストOSの環境依存でパスを指定する部分については別途「.env」ファイルを作っている。Dockerの設定を完璧に行っても、唯一各環境で書き換える必要がある部分。docker-compose.ymlと同フォルダに作成している
- `build:`
    - どのDockerfileをbuildするかをここに書く
    - `context`にDockerfileのあるフォルダを指定。今回の場合はdocker-compose.ymlと同フォルダに作成している
    - `dockerfile`に、このコンテナ（今回の場合get-news）をbuildするために利用するDockerfile物理名称を指定
- `volumes:`
    - 各種ローカルからコンテナへのマウント情報。今回の場合は、①shell（後述）、②作成したモジュール郡、③AWSクレデンシャルをマウントする
    - ①shellについては各開発者環境で共通して利用するため、docker-compose.ymlと同フォルダに作成し、envを介さずに相対パスを記載
    - ②作成したモジュール郡および③AWSクレデンシャルについては各開発者環境で異なるため、envで指定した環境変数を記載
- `command: >`
    - デプロイ後最初に実行するコマンドを記載。今回はshell（後述）を呼び出すことで、[2章](https://qiita.com/Shunichirou/items/dca96dd027674feea835)で作成したPythonモジュールの管理や、MeCabの辞書コンパイルなどを行う

参考（.envファイル中身）

```
# ------------------以下ホストの環境に合わせて変更してください ------------------

# ソースディレクトリへのPath. 各コンテナは以下Pathを参照して環境を構築します.
# 指定したディレクトリは、コンテナ上からmountされ参照されます.
get_news_root_dir=/home/username/backEndLogic

# AWSのcredential情報を記載したファイルパス
host_aws_credential=/home/username/.aws

```

インフラ周りの設定情報についても、docker-compose.ymlに書く。たとえばファイルオープン数が最大に達してしまい、コンテナにもっとリソースを割きたい場合は、volumeと同じ階層に以下のように追記すると良い。

```
      ulimits:
        nproc: 65535
        nofile:
          soft: 20000
          hard: 40000
```

※この場合、後述するshellの中にスタックサイズの開放コマンド`$ ulimit -s unlimited`を書く必要がある。


## 4.shellの記載
1～3までで、コンテナ環境の構築や、コンテナ環境へのPythonモジュールのマウントは完了した。しかし、デプロイ後に実際にPythonモジュールを実行したり、MeCabの設定が必要だったりするので、shellを外だしで作成する。

#### get_news.sh

- MeCabの辞書がローカルで更新されていた場合、Dockerコンテナ上で毎回ユーザ辞書を更新する必要があるため、shellに任せました
- 作成したPythonモジュールを実行するために、main.pyに飛ばしました（ここは各々好きに作ればよいと思います）

```get_news.sh
# ユーザ辞書のコンパイル
cd /root/backEndLogic/mecabDict

/usr/lib/mecab/mecab-dict-index \
  -d /var/lib/mecab/dic/mecab-ipadic-neologd \
  -u /root/backEndLogic/mecabDict/select_dic.dic \
  -f utf-8 \
  -t utf-8 select_dic.csv

# mecabの設定ファイルにユーザ辞書を追記
cd /etc
sed -i -e 's@.*userdic.*@userdic = /root/backEndLogic/mecabDict/select_dic.dic@' mecabrc

# モジュールのおいてあるパスへ移動
cd /root/backEndLogic/

# News収集機能を実行
python /root/backEndLogic/main.py
```


#### main.py
```main.py
import subprocess

def main():

    subprocess.check_call(['python','ニュース取得機能.py'])
    print("complete get_news")

    subprocess.check_call(['python','ニュース選定機能.py'])
    print("complete select_target")

if __name__ == '__main__':
    main()
```
※実際のアプリケーションとは異なるため、簡易的に「ニュース取得機能.py」、「ニュース選定機能.py」を呼び出すような記載としています

# docker-compose実行
ここまでくれば、後は2つコマンドを実行してあげるだけです。その前に、現状のフォルダ構成はこうなっています。

```
.../
    - shell
        - get_news.sh
    - Get_news_Dockerfile
    - docker-compose.yml
    - .env
```
docker-compose.ymlと同フォルダに、Dockerfileや.env、shellが置いてあります。（docker-compose.ymlの記載次第）

いよいよコマンドを実行します。実行するコマンドはたったの2つ。（コンテナが増えても同様に2つです）
#### build
プロキシ環境下
 - `$ docker-compose build --build-arg https_proxy=${https_proxy} --build-arg http_proxy=${http_proxy} get-news`
非プロキシ環境下
 - `$ docker-compose build get-news`

#### 実行結果

```
Step 1/9 : FROM python:3.6.5
 ---> 9a58cce9b09f
Step 2/9 : RUN apt-get update && apt-get upgrade -y
 ---> Using cache
 ---> 0c353486cd74
Step 3/9 : RUN pip install --upgrade pip
 ---> Using cache
 ---> 43abb7ace4c6
Step 4/9 : RUN pip install beautifulsoup4==4.6.3
 ---> Using cache
 ---> 8429fda55ce3
Step 5/9 : RUN apt-get install mecab mecab-ipadic libmecab-dev -y
 ---> Using cache
 ---> e3f054b82472
Step 6/9 : RUN pip install mecab-python3==0.7
 ---> Using cache
 ---> d3b3d6ecba95
Step 7/9 : RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git /usr/src/mecab-ipadic-neologd   && /usr/src/mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -y -p /var/lib/mecab/dic/mecab-ipadic-neologd   && rm -rf /usr/src/mecab-ipadic-neologd
 ---> Using cache
 ---> 1d3510550bd8
Step 8/9 : RUN pip install boto3==1.7.4
 ---> Using cache
 ---> 015cd5134280
Step 9/9 : WORKDIR ./
 ---> Using cache
 ---> f7d29b04626e
Successfully built f7d29b04626e
Successfully tagged docker_get-news:latest
```

#### コンテナ起動＆デプロイ
 - `$ docker-compose up`

#### 実行結果抜粋
```
get-news_1   | 
get-news_1   | done!
get-news_1   | 
get-news_1   | complete read parent html : http://finovate.com/blog
get-news_1   | 
...
```

続く