Dockerで開発環境を仮想化する

# ことの発端
時々、提供されるソフトのバージョンが変わってコンパイラのバージョンが変わることがあったりします。
しかし、自分たちの膨大な数のmakefileにはgccとかclangとかしか書かれてないのです。
これでは去年の製品を再ビルドしようにも、シンボリックリンク先が新環境を指しているのでダメです。

しかし、製品サポート案件で容赦なく過去モデルでのバグ修正チケットが回ってきます。
先輩に聞くと「コンパイラやライブラリのディレクトリ名をリネームする」と。
なん、だと……という感じです。今の環境が正しいのかすら保証できません。

これでは、バグ以前に昔のモデルを再現することすら難しいです。
もう、xx年用ビルド環境を個別に用意するしかありません。
しかし、マシンを複数用意してくれる訳もなく（してもらってもスペース的に困りますが）、そこで仮想化の手段に手を出し始めました。
<font color=red>**幸いにも、マシンに基本的人権は備わっていたのです。**</red>

# 注意点1: コンテナ型VMの特徴は分かっておいてね

Dockerはコンテナ型VMと言われるモノで、プロセスとして動きます。
つまり、ホストとゲストが<font color=red>Kernelを共有する</font>ことになります。
これが許容できる場合にのみ行ってくださいね。

ホストと開発環境が違うKernelで動いている場合は、KernelからVMするホスト型VM（※1）を使う必要があります。
その場合は、Vagrantで自動化できるのでそちらを勉強してみてください。

※1...VirtualBoxやVM Wareなど

# 注意点2: DockerfileはLintしようね
Dockerfileの更新を最新の記述方式に合わせるようにはわざわざ<font color=red>追従していません</font>。

[hadolint](https://github.com/hadolint/hadolint)というLintソフト（書式を検証してくれる）があるので、それを正義にしてください。
英語ですけど、すっごい簡単にコマンド一発で動かせます。

# 注意点3: この記事、あんまりよろしくないらしい
今のチームに、人間辞めてそうな人が帰ってきたんです。
その人も勝手にDocker使って環境立ち上げてて、「おっやるやん？」とおこがましくも思っていました。
この「環境を仮想化しよう運動」が実りを見せ始めて、Dockerfileをもらったんですけど、意味不明でした。

Dockerのポリシーである「Dockerは一瞬だけ立ち上げて、仕事が終わったら潰す」を本当に地で実践していたんです。
**<font color=red>`make`コマンドを叩いた一瞬だけ立ち上がってビルドし、ビルドが終わればコンテナを潰している</font>**んです。
**Makefileに`docker`コマンドを書く**なんて発想微塵もしなかったですね(´･_･`)

完全に僕の負けです。（別に勝負してるわけじゃないんですけど
この運動が最後まで完走出来たら、核なんだけど外部に公開できる部分だけを抽出して、それが意味を持てるレベルならQiitaに新しく書こうと思います。。。
最近、TILに書いてるし、そっちかもしれませんけど。。。

# 環境
**OS:**Windows10 -(Virtual Box)-> ubuntu 16.04 -(Docker)-> ubuntu 14.04
**Docker:**17.03

# Dockerの導入

`apt-get`で手に入るDockerはなんだか古い感じがしたので、[本家のドキュメント通りに導入しました](https://www.docker.com/community-edition#/download)。
今は、Docker Engineって名前じゃなくて、２種類ぐらいに分かれているようです
[Docker blogに記事が上がっています](https://blog.docker.com/2017/03/docker-enterprise-edition/)

|Docker|説明|
|:----:|:--|
|Docker Engine|旧バージョンのDocker。使わないでってあります。|
|Docker CE<br>*Community Edition*|個人用の無料版です。<br>Visual Studio Communityみたいなもんでしょうか
|Docker EE<br>*Enterprise Edition*|法人用のサポート付きです。あんまり良く分かりません。|

# Dockerする前に
dockerを色々やっていく前に、Proxy環境だとのっけからコケるので一つだけ。
**Proxy環境でない方は、飛ばしてください。**

## Proxy設定
これから使うFROMコマンドでは、[docker hub](https://hub.docker.com/)あたりから該当するイメージをDLします。
しかし、Proxyサーバーをdockerが認識していないと外部ネットワークにつながらなくて実行できません。
こっちにまとめました。

upstartの人やsystemdの人を分けてあります。
※systemdちょっと自信ないです。今度systemd環境でやる機会があるので修正予定はあります。（多分）
[Proxy環境でdockerを外に繋ぐ方法](https://qiita.com/Riliumph/items/921e76444ea6ba145294)

# Dockerやるぞい
それでは、これからDockerfileを作っていきます。
好きな作業ディレクトリで行いましょう。
私は$HOME/Dockerでやってます。

## Dockerfileの作成

ベースとなるOSの選択とディレクトリの移動をしておきましょう。
ディレクトリが存在しなくても勝手に作ってくれます。

```Dockerfile:Dockerfile
# 仮想化するベースOS
FROM ubuntu:14.04
# 誰が作ったかは入れておきましょう
MAINTAINER tester
# ディレクトリの移動
WORKDIR /home/tester
```

## イメージをビルドする

Dockerfileを作ったら、それをイメージにします。

```bash:HOST
docker build -t qiita:0.1 ./
```

- -t/--tag REPOSITORY NAME:TAG NAME  
  イメージのリポジトリ名:タグ名を設定できます。
- ./  
  Dockerfileのあるパスを指定します。  
  今は、Dockerfileのあるパスで作業している前提なのでカレントディレクトリを指定しています。



## ボリュームの設定
今回のDockerはビルドサーバーとして用意しています。
Dockerを止めた時、ビルドした結果も消えてしまいます。非常に残念ですね。
そこでホスト側のディレクトリをコンテナ側でマウントして、ホスト側に保存してやりましょう。

Dockerの世界では共有ディレクトリを「ボリューム」と言うそうです。
ボリュームは、コンテナ側ではなくてホスト側から指定するのでコマンド上で行います。

```:HOST
$ docker run --name test -it --rm -v $HOME/Docker:/home/tester/share qiita:0.1
```
- -it  
 [teratailのこの質問](https://teratail.com/questions/19477)が一番わかり易かったです。
- --rm  
  コンテナの終了時に、dockerプロセス欄からも消すオプション
- --volume / -v ホスト側のパス:コンテナ側のパス  
  共有ディレクトリを貼るオプション

これで、ホストとコンテナは繋がりました。
このディレクトリに保存すると、コンテナを停止してもファイルが残ります。
安心してgit cloneしていいです。

## アクセス権について

dockerを起動したので、共有ディレクトリにファイルを保存してみます。

```:CONTAINER
# touch /home/tester/share/from_root_docker.txt
# exit
```

ホスト側からアクセスしてみましょう。

```:HOST
$ ll $HOME/Docker
-rw-r--r-- 1 root root 0 yy-mm-dd HH:MM:SS from_root_docker.txt
```

確かにコンテナをexitしてもファイルが残り続けています！！
コンテナの寿命という円環の理から外れることができました！

……が、どう見てもrootユーザー、rootグループ。
一般ユーザーにはr権限しかありません。ありがとうございました。
これでは、コンテナ側でgit cloneしたファイルをホスト側で変更するのに毎回sudo権限を使う必要があります。
メンドウですね……

## 一般ユーザーの追加
root権限でファイルが生成されるのは非常にメンドウなので、一般ユーザーでやってみましょう。
普通にユーザーを追加するコマンドを自動実行してもらいます。

```Dockerfile:Dockerfile
# 仮想化するベースOS
FROM ubuntu:14.04
# 誰が作ったかは入れておきましょう
MAINTAINER tester

# 各環境変数を設定
ENV USER tester
ENV HOME /home/${USER}
ENV SHELL /bin/bash

# 一般ユーザーアカウントを追加
RUN useradd -m ${USER}
# 一般ユーザーにsudo権限を付与
RUN gpasswd -a ${USER} sudo
# 一般ユーザーのパスワード設定
RUN echo "${USER}:test_pass" | chpasswd
```

ハマったポイント

- **useraddコマンドには-mが必須** 
useraddコマンドだけだと、ホームディレクトリがアクセス権<font color=red>root:root</font>で作られてしまいます。 
理由はよくわかりませんが、作業すべてにsudoを要求されてとてつもなく不便です。
<font color=red>**adduser**</font>コマンドを使うか、<font color=red>**useradd -m**</font>を使うとアクセス権<font color=red>tester:tester</font>でホームディレクトリが作られました。
※[adduserとuseraddの違いはこちら](http://qiita.com/amedama/items/f45677cd944c42cde839)
簡単に言うと、adduserは対話的、useraddは独話的とのことなので、useraddを使っています。

- **chpasswdは引数を取らない** 
chpasswdなんてコマンドは初めて知ったのですが、root限定コマンドらしいです（当然）
複数ユーザーのパスワード一括設定ができます。 
が、これ、引数を取りません。データファイルを<font color=red>パイプで流す</font>みたいです。
今回は一人分なのでechoからパイプしました。

```:HOST
$ docker build --tag qiita:0.2 ./
```

## いざ、一般ユーザーでコンテナを起動！
一般ユーザーで起動する方法は、オプション指定する方法とDockerfileに書いてしまう方法の２種類があります。
色々な運用があるので一概には言えないと思いますけど、毎回userを選択させられるのはメンドウなので２番目の手法がオススメです。

### 方法１：オプションで起動する方法

ハマったポイントなんですけど、Dockerでユーザー指定するとき、
私はホスト側のアカウント設定をロードして、その設定を引き継いでくれるのかな、なんて思っていたんです。
なんか、resolv.confはそんな感じらしいじゃないですか。
<font color=red>**ホスト側設定(/etc/passwd)を引き継がない**</font>ので注意です。  

Dockerのコマンドでユーザーオプション(-u)を使う場合は、<font color=red>出来上がる仮想環境下のアカウント設定にユーザーデータを作る</font>必要があります。
まぁ、オプションを使わなくても設定しといて問題ない（と思う）ので設定しときましょう。

``` Dockerfile:Dockerfile
# 仮想化するベースOS
FROM ubuntu:14.04
# 誰が作ったかは入れておきましょう
MAINTAINER tester

# 各環境変数を設定
ENV USER tester
ENV HOME /home/${USER}
ENV SHELL /bin/bash

# 一般ユーザーアカウントを追加
RUN useradd -m ${USER}
# 一般ユーザーにsudo権限を付与
RUN gpasswd -a ${USER} sudo
# 一般ユーザーのパスワード設定
RUN echo "${USER}:test_pass" | chpasswd
# ログインシェルを指定（アカウント情報の作成）
RUN sed -i.bak "s#${HOME}:#${HOME}:${SHELL}#" /etc/passwd
```

```:HOST
$ docker run --name test --u tester -it --rm -v $HOME/Docker:/home/tester/share qiita:0.2
```

- --user / -u ユーザー名  
  <font color=red>コンテナ内の/etc/passwd</font>に登録されているユーザーでログインする  

### 方法２：Dockerfileに書いておく方法

```Dockerfile:Dockerfile
# 仮想化するベースOS
FROM ubuntu:14.04
# 誰が作ったかは入れておきましょう
MAINTAINER tester

# 各環境変数を設定
ENV USER tester
ENV HOME /home/${USER}
ENV SHELL /bin/bash

# 一般ユーザーアカウントを追加
RUN useradd -m ${USER}
# 一般ユーザーにsudo権限を付与
RUN gpasswd -a ${USER} sudo
# 一般ユーザーのパスワード設定
RUN echo "${USER}:test_pass" | chpasswd
# ログインシェルを指定
sed -i.bak -e "s/${HOME}:/${HOME}:${SHELL}" /etc/passwd

# 以降のRUN/CMDを実行するユーザー
USER ${USER}
# 以降の作業ディレクトリを指定
WORKDIR ${HOME}
```
※なんかkobitoでシンタックスハイライトしてくれませんけど、ちゃんと動きます。

USERコマンドを使うと、以降のRUNコマンド、CMDコマンドをそのユーザーで行うようになるようです。
docker runコマンドでuserオプションを指定しなくても良くなります。
このコマンドは**<font color=red>対象のUSERでログインする</font>**と思えばいいんじゃないでしょうか。


```:HOST
$ docker build --tag qiita:0.2 ./
$ docker run --name test -it --rm -v $HOME/Docker:/home/tester/share qiita:0.2
```

## ファイルを作成してみる

git cloneとかしてみてもいいですけど、適当にtouchコマンドで空ファイルを作ってみます。

```:CONTAINER
$ touch /home/tester/share/from_tester_docker.txt
$ exit
```

## ホスト側でファイルを確認する

```:HOST
$ ls -l
-rw-r--r-- 1 root         root         0 YY-MM-DD hh:mm:ss from_root_docker.txt
-rw-r--r-- 1 host_user    host_user    0 YY-MM-DD hh:mm:ss from_tester_docker.txt
```

あ、あれ！？ コンテナ側のtesterユーザーでファイル作ったのに、ホスト側のユーザー名になってる！？
多分、ファイルはuid/gidなどの**番号で管理**してるんじゃないでしょうか。

|場所|ユーザー名|UID|GID|
|:--|:--|:--:|:--:|
|コンテナ|tester|1000|1000|
|ホスト|ログインユーザー名|1000|1000|
※uid/gidはデフォルト1000番で、ユーザーが増えるごとに1001, 1002...と増えていくようです。
 
番号でしか管理していないから、ホストで見た時はホスト側のユーザーが所有者になるんじゃないかな、と。
<font color=red>**根拠全く無いですけど。**</font>

# 終わり
gccとかclangとかgolangとかpythonとかのソフトウェアは別途インストールしてくださいね。
その時は、ぜひともDockerfileに書いてやってください。

それでは、よいdocker生活をﾉｼ