boot2dockerで 素のCentOS Imageから便利なchef開発ができるまでの準備


はじめに
======

MacでDockerを使いたい場合、Dockerから正式に出ているboot2dockerというちょっと便利なツールがあります。
これを使ってChefのレシピをDocker Imageに対して適用しながら開発する環境の準備について共有します。
いろいろアレンジはあると思いますが、ご参考まで。

この方法の良い所
-------

* MacのLocalファイルにあるレシピをDocker Imageに適用できるので開発が楽です
	* Docker Imageに対しては `knife-solo` を使ってレシピを送ります
* 「やり直し」や「適用後の状態の確認」を比較的簡単に行えます
* Dockerfile+chef で Docker Imageを作成していく際にとても便利です

構成イメージ
----------

最終的には以下のような構成にします。Windowsでも同様の構成が取れると思います。
また、見てもわかるようにDockerはVirtualBox上で動いている必要はなく、Remote Serverのdockerに対してもMacのLocal環境はほとんど変更せずに作業ができます(TCPさえ通ればOK）。

![システム構成イメージ.png](https://qiita-image-store.s3.amazonaws.com/0/11124/19597c2c-a868-0017-150a-85e7a3a029f3.png "システム構成イメージ.png")


各Version
-------

* MacOS: 10.9.2
* VirtualBox: 4.3.8
* boot2docker: 0.6.0
* docker:  0.8.0, build cc3a8c8d8ec57e15b7b7316797132d770408ab1a
* chef: 11.10.2
* knife-solo: 0.4.1

手順
====

* (前準備) boot2dockerのInstall と setup
* (前準備) Host Only アダプターの有効化
* (本題) Docker Imageの作成
* (本題) knife-solo環境の準備
* (本題) knife-solo 実行！



(前準備) boot2dockerのInstall
---------------------------

### VirtualBoxのInstall
まだ、VirtualBoxをInstallしていなければ、まず、下記からVirtualBoxをInstallします。
https://www.virtualbox.org/wiki/Downloads

### Homebrew の Install
まだ、Homebrew をInstallしていなければ、何も考えずに

```bash
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
```

と実行しておきます。(公式サイト： http://brew.sh/ )

### boot2docker の Install

```bash
brew update
brew install boot2docker
```

これで boot2docker の導入は終わりです。
※ちなみに、MacのDocker Clinetも一緒にInstallされます。

### boot2docker の setup と 起動

boot2docker は VirtualBox上にVM Imageを作成し、その上でさらにdockerを動かします。
多分内部的には、vagrantを使っているのだと思います。

最初にVM Imageをダウンロードする必要があるので、以下のように実行します。

```bash
boot2docker init
boot2docker up
```

`boot2docker up`した後にVirtualBoxのGUIを起動すると、
![boot2dockerVM](https://qiita-image-store.s3.amazonaws.com/0/11124/e5bdce2e-3f9e-b8a4-c09e-211fdff4be9e.png "boot2dockerVM")
が動いているのがわかります。

docker は VirtualBoxのVMで起動しているので、MacのDocker Clientはそこと通信して動作する必要があります。
VMの設定で4243ポートがNATされているので、以下の様な設定を`.zshrc`や`.bashrc`に書いておくと良いでしょう。
※ 現在のShellでも実行しておくことも忘れずに。

```bash
# .zshrc or .bashrc
export DOCKER_HOST=tcp://127.0.0.1:4243 
```

(前準備) HostOnly アダプターの有効化
------------------------

knife-solo で DockerコンテナのSSHに直接繋ぎたいので、VirutualBoxのGUIでHostOnlyアダプターを有効にします。
NATを設定しても良いですが、boot2dockerVMが最初からよしなに設定してくれているので、後々便利そうなのでHostOnlyアダプターを使います。

### まだホストオンリーアダプターを作ったことがない場合
下記ブログの「ホストOSにネットワークを作成」などが参考になると思います。
http://blog.kjirou.net/p/2171
ホストオンリーアダプタは、１回作れば他のVMでも使えるので作っておきましょう。

### ホストオンリーアダプターを設定

一旦 boot2dockerVMを停止します。

```bash
boot2docker stop
```

その後、VirutualBox GUIで 「boot2docker-vm」を選択し、「設定」→「ネットワーク」 から下記のように選択します。

![boot2dockerVMのホストオンリーアダプタ.png](https://qiita-image-store.s3.amazonaws.com/0/11124/0232eb9a-8240-1479-47e0-11b4feb73bb2.png "boot2dockerVMのホストオンリーアダプタ.png")

再度、boot2dockerVMを起動してIPを調べておきます。

```bash
% boot2docker up
% boot2docker ssh
                        ##        .
                  ## ## ##       ==
               ## ## ## ##      ===
           /""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
           \______ o          __/
             \    \        __/
              \____\______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
boot2docker: 0.6.0
docker@boot2docker:~$ ifconfig
docker0   Link encap:Ethernet  HWaddr FE:AF:63:C4:4F:02  
          inet addr:10.1.42.1  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::ecb3:3cff:fe83:cb3f/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:26537 errors:0 dropped:0 overruns:0 frame:0
          TX packets:64098 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:1354694 (1.2 MiB)  TX bytes:60164533 (57.3 MiB)

eth0      Link encap:Ethernet  HWaddr 08:00:27:99:66:E4  
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fe99:66e4/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:89688 errors:0 dropped:0 overruns:0 frame:0
          TX packets:49580 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:61443437 (58.5 MiB)  TX bytes:31615700 (30.1 MiB)

eth1      Link encap:Ethernet  HWaddr 08:00:27:6A:AD:03  
          inet addr:192.168.33.100  Bcast:192.168.33.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fe6a:ad03/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1628 errors:0 dropped:0 overruns:0 frame:0
          TX packets:841 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:534878 (522.3 KiB)  TX bytes:129582 (126.5 KiB)
          Interrupt:16 Base address:0xd040 
```

大抵、`eth1`になると思います。上記の場合ですと、`192.168.33.100` がboot2dockerVMのIPになります。


(本題) Docker Imageの作成
---------------------

以下の様な設定で Chefが使えるDocker Imageを作っていきます。

* CentOS 6.4ベース
* sshd の Install
* centos ユーザを作成し nopasswordでのsudoを許可

### dockerfile 用のDIR と SSH鍵の作成

```bash
mkdir centos64-base
cd centos64-base
ssh-keygen -t dsa -C "centos64-base" -f centos 
```

次に Dockerfileを作ります。

```Dockerfile
FROM centos:6.4
MAINTAINER mokemokechicken <mokemokechicken@twitter>

ENV PATH $PATH:/usr/bin

RUN yum update -y

# Timezone to UTC
RUN cp /usr/share/zoneinfo/UTC /etc/localtime

# Chef & git
RUN curl -L https://www.opscode.com/chef/install.sh | bash
RUN yum install -y git

# SSH Server
RUN yum install -y zsh openssh-server sudo
RUN mkdir -p /var/run/sshd
RUN ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa
RUN ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa

# add centos user (password=password)
RUN useradd -d /home/centos -m -s /bin/zsh centos
RUN echo centos:password | chpasswd
RUN echo 'centos ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN mkdir -p /home/centos/.ssh
ADD centos.pub /home/centos/.ssh/authorized_keys
RUN chown centos:centos -R /home/centos
RUN chmod 700 /home/centos/.ssh
```

### Docker Image の Build

割と何度もBuildすることになるので、Makefileなど作っておくと便利です。この辺は適当に。

```Makefile
IMG=mokemokechicken/centos64-base

build:
        docker build -t $(IMG) .

rebuild:
        docker build --no-cache -t $(IMG) .

delete:
        docker rmi $(IMG)

push:
        docker push $(IMG)
```

では、Buildを実行しましょう。

```bash
make build
```

成功しましたか？

(本題) knife-solo環境の準備
---------------

LocalのChefの環境構築や knife-solo のInstall方法は省略します。gemやbundlerなどでサクッと作っておきましょう。
今回は bundler で vendor/bundle などにknife-soloがInstallされている想定で話を進めます。

###余談
```
Dockerで起動しているコンテナに chefのレシピを送るために、`docker run` の `-v`オプションでマウントできれば簡単なのですが、間にboot2dockerのVMが挟まっているためにそれができません。docker run で -v をした場合、boot2dockerVMとDockerコンテナの間でファイル共有されます。
Macとboot2dockerの間でもファイル共有すればいいのかもしれませんが、少しやり過ぎな感じがあるので、普通にknife-soloを使ったSSHでの転送を使うことにします。
```

### 実行スクリプトの作成

Dockerコンテナにknife-soloを接続するには、

* Dockerコンテナの起動 with sshd
* knife-solo 実行
* Dockerコンテナの停止
* 実行後のコンテナのCommit (実行検証用に)

というような手順が必要です。
面倒なのでスクリプト化します。

ちなみに、`knife-docker` という gem もあったのですが、IPアドレスの変更方法がわからなかったので使いませんでした。

```bash:bin/chef-docker 
#!/bin/bash

IMAGE=$1
shift
ARGS=$@
EXTRA_ARGS=""

if [[ "$ARGS" == "" ]]; then
    cat <<EOM
= Usage: $0 <Docker Image> <knife-solo cook args>

= Environment variables

    DOCKER_PORT:  Docker container SSH port (default: 8022)
    SSH_IP     :  knife-solo ssh IP         (default: 127.0.0.1)
    SSH_USER   :  knife-solo ssh user       (default: centos)
    SSH_KEY    :  knife-solo ssh key        (optional)

= Environment file

    if ~/.chef-docker-env exists, evaluated automatically.
    ~/.chef-docker-env must be a shell script.

= To See knife solo cook help

    bundle exec knife solo cook --help

EOM
    exit
fi

CHEF_DOCKER_ENV_FILE=${CHEF_DOCKER_ENV_FILE:-~/.chef-docker-env}

cd $(dirname $0)/..

if [[ -e $CHEF_DOCKER_ENV_FILE ]]; then
    . $CHEF_DOCKER_ENV_FILE
fi

DOCKER_PORT=${DOCKER_PORT:-8022}
SSH_IP=${SSH_IP:-127.0.0.1}
SSH_USER=${SSH_USER:-centos}

if [[ "$SSH_KEY" != "" ]]; then
    EXTRA_ARGS="-i ${SSH_KEY} ${EXTRA_ARGS}"
fi


NEW_IMAGE=last

echo docker run -d -p ${DOCKER_PORT}:22 ${IMAGE} /usr/sbin/sshd -D
DID=$(docker run -d -p ${DOCKER_PORT}:22 ${IMAGE} /usr/sbin/sshd -D)

if [[ $? == 0 ]]; then
    rm nodes/*.json 2>&1 >/dev/null

    echo bundle exec knife solo cook ${SSH_USER}@${SSH_IP} -p ${DOCKER_PORT} --no-berkshelf $EXTRA_ARGS $ARGS
    bundle exec knife solo cook ${SSH_USER}@${SSH_IP} -p ${DOCKER_PORT} --no-berkshelf $EXTRA_ARGS $ARGS

    echo docker stop $DID
    docker stop $DID > /dev/null
    (docker images | grep ${NEW_IMAGE} && docker rmi ${NEW_IMAGE}) 2>&1 > /dev/null
    docker commit $DID ${NEW_IMAGE} > /dev/null

    cat <<EOM
Finish Successfully!
If you want to login the chef configured image,

docker run -t -i ${NEW_IMAGE} /bin/bash

EOM

else
    echo docker run fail.
fi
```

knife-soloで接続するときには boot2dockerVMのIPやSSH鍵(先ほどssh-keygenで作った秘密鍵)が必要になります。
環境変数で指定できるようにしてありますが、毎度指定したりするのも面倒なので `~/.chef-docker-env`にファイルがあればそれを読み込むようにしてあります。
例えば以下のように書いておくと便利です。

```bash:~/.chef-docker-env 
export SSH_IP=192.168.33.100
export SSH_KEY=~/.ssh/centos
```

(本題) knife-solo 実行
----------

やっと準備は終わりです。

### 実行
以下のようにして、レシピを実行できます。

```bash
bin/chef-docker mokemokechicken/centos64-base -r 'recipe[mysystem]' 
```

上記の `mokemokechicken/centos64-base` 以降の引数は `knife solo cook` にそのまま渡されます。
オプション周りは使いやすいように都度都度変更していくのかと思います。

### 結果の確認

レシピ実行後のコンテナを `last` という名で自動Commitするようにしてあるので、

```bash
docker run -t -i last /bin/bash
```

とすれば、コンテナに入ることができます。

### やり直しと続きからのレシピ適用

やり直したければ、前と同じImage(`mokemokechicken/centos64-base`)を再度`chef-docker`で指定すればよく、
続きから行いたければ、Image `last` を`chef-docker`で指定すれば良いです。
レシピ実行時間短縮のために続きからやりたいことも多いので、結構重宝します。
最初から通してやりたければ、すぐにできるというのも魅力ですね。