Packerを使ってDockerのイメージを作成する

Docker便利ですね。
仮想マシンを立ち上げるまで大袈裟ではないけど、母艦のマシンを汚したくないときはDockerでコンテナを立ち上げると便利です。

Dockerを使うにはイメージが必要なわけで、デフォルトではDockerfileでプロビジョニングするわけですが、せっかくなら最近のトレンドである専用ツールであるAnsibleを使ってプロビジョニングしたいものです。（playbookの共有ができますし。）
Vagrantでおなじみのhashicorpからリリースされている[Packer](https://www.packer.io/)を使うと、それができます。

# Pakcerとは？
Packerは仮想コンテナで使用されるイメージ作成に特化した自動実行ツールです。
Dockerだけではなく、

- EC2 (AMI)
- Azre
- DigitalOcan
- Docker
- Google Compute Engine
- OpenStack
- Parallels
- QEMU
- VirtualBox
- VMware

とほぼ全ての仮想化コンテナに対応しています。

# 必要なもの
[ダウンロードページ](https://www.packer.io/downloads.html)からパッケージをインストールすればOK。

# 使い方
イメージの作成方法を記載したPackerの設定ファイル（json）を作成します。

設定ファイルは以下の3つのセクションから構成されています。

- builders: どの仮想コンテナ用のコンテナを作成するか。
- provisioners: どうプロビジョニングするか。
- post-processors: 作成したイメージをどうするか？

```json:packer.json
{
    "builders":[
        ...
    ],

    "provisioners":[
        ...
    ],

    "post-processors": [
        ...
    ]
}
```
各セクションには復数のアイテムを指定することが可能です。

## buildersセクション
```json
{
    "builders":[{
        "type": "docker",
        "image": "basebox/common:0.1",
        "export_path": "controller.tar",
        "pull": false
    }]
}
```

今回はDockerのイメージを作成するのでtypeに"docker"を、元となるイメージの名前をimageに指定します。
export_pathはイメージの中間保存するためのくファイル名を指定しますが、最後に自動的に削除してくれますので適当な名前を指定しておけば良いでしょう。

local repositoryのイメージを使う場合は`pull: false`の指定が必要です。

## provisionersセクション
typeにどのようなプロビジョナを使うかを指定します。

ansibleの場合、以下の2つのタイプが指定可能です。

- ansible-local: イメージ内に既にインストールされているansibleを使って、自分自身に対してプロビジョニングする。
- ansible: 対象イメージにsshでリモート接続してプロビジョニングする。（通常のansibleと同じ）
 - EC2などはこちらを使う。

```json
{
    "provisioners":[{
        "type": "shell",
        "inline": [
            "apt-get -y update",
            "apt-get install -y --no-install-recommends ansible",
            "mkdir -p /tmp/ansible-local"
        ]
    }, {
        "type": "file",
        "source": "files",
        "destination": "/tmp/ansible-local"
    }, {
        "type": "ansible-local",
        "playbook_file": "playbook.yml",
        "staging_directory": "/tmp/ansible-local"
    }]
}
```
今回は前者のansible-localを使います。
そのため、ansible(-local)プロビジョナの前にshellプロビジョナを置いて、ansibleをインストールする必要があります。

また、ansible内でcopyモジュールを使う用に、その対象ファイルをイメージ内に保存するディレクトリ（`/tmp/ansible-local`）を作成して、fileプロビジョナでそのディレクトリに転送しておく必要があります。

ここまで準備しておけば後は簡単。
ansible(-local)プロビジョナで`playbook`を指定すればOK。
ansibleのインベントリファイルはpackerが自動生成してくれます。

## post-processorsセクション
```json
{
    "post-processors": [{
        "type": "docker-import",
        "repository": "category/image",
        "tag": "0.1"
    }]
}
```
出来上がったイメージをどうするかを指定します。
今回は"docker-import"でローカルにイメージ登録しておくことにします。


### 最終的なpacker.json

```json:packer.json
{
    "builders":[{
        "type": "docker",
        "image": "basebox/common:0.1",
        "export_path": "controller.tar",
        "pull": false
    }],

    "provisioners":[{
        "type": "shell",
        "inline": [
            "apt-get -y update",
            "apt-get install -y --no-install-recommends ansible",
            "mkdir -p /tmp/ansible-local"
        ]
    }, {
        "type": "file",
        "source": "files",
        "destination": "/tmp/ansible-local"
    }, {
        "type": "ansible-local",
        "playbook_file": "playbook.yml",
        "staging_directory": "/tmp/ansible-local"
    }],

    "post-processors": [{
        "type": "docker-import",
        "repository": "category/image",
        "tag": "0.1"
    }]
}
```

# Packerの実行
実行は非常に単純です。

```shell
$packer build <ビルドファイル>.json
```

コンテナの生成から、プロビジョニング、イメージの保存までを自動的に行なってくれます。

## 実行結果
```shell
$ packer build -on-error=abort packer.json
docker output will be in this color.

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu:16.10
    docker: 16.10: Pulling from library/ubuntu
    docker: Digest: sha256:063e14a48414f6e66ec0e6570ae0f846623e08ff9416b280f560898bac7e7e63
    docker: Status: Image is up to date for ubuntu:16.10
==> docker: Starting docker container...
    docker: Run command: docker run -v /Users/h_toda/.packer.d/tmp/packer-docker793412964:/packer-files -d -i -t ubuntu:16.10 /bin/bash
    docker: Container ID: dd34a7aeeba9a3371d6c7ba84a215b145afaaea5d013676d6d1111f5647f9f12
==> docker: Provisioning with shell script: /var/folders/d1/c3mwd9kn20vdng53mywr181xm5hflj/T/packer-shell812644666
    docker: Get:1 http://security.ubuntu.com/ubuntu yakkety-security InRelease [92.2 kB]
    docker: Get:2 http://archive.ubuntu.com/ubuntu yakkety InRelease [247 kB]
    docker: Get:3 http://security.ubuntu.com/ubuntu yakkety-security/universe Sources [3526 B]

(snip)

    docker: Setting up ansible (2.1.1.0-1) ...
    docker: Processing triggers for libc-bin (2.24-3ubuntu1) ...
    docker: Processing triggers for ca-certificates (20160104ubuntu1) ...
    docker: Updating certificates in /etc/ssl/certs...
    docker: 173 added, 0 removed; done.
    docker: Running hooks in /etc/ca-certificates/update.d...
    docker: done.
==> docker: Uploading files => /tmp/ansible-local
==> docker: Provisioning with Ansible...
    docker: Creating Ansible staging directory...
    docker: Creating directory: /tmp/ansible-local
    docker: Uploading main Playbook file...
    docker: Uploading inventory file...
    docker: Executing Ansible: cd /tmp/ansible-local && ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook /tmp/ansible-local/playbook.yml -c local -i /tmp/ansible-local/packer-provisioner-ansible-local607385102
    docker:
    docker: PLAY [all] *********************************************************************
    docker:
    docker: TASK [setup] *******************************************************************
    docker: ok: [127.0.0.1]
    docker:
    docker: TASK [debug] *******************************************************************
    docker: ok: [127.0.0.1] => {
    docker: "msg": "Hellow, World!"
    docker: }
    docker:
    docker: PLAY RECAP *********************************************************************
    docker: 127.0.0.1                  : ok=2    changed=0    unreachable=0    failed=0
    docker:
==> docker: Exporting the container
==> docker: Killing the container: dd34a7aeeba9a3371d6c7ba84a215b145afaaea5d013676d6d1111f5647f9f12
==> docker: Running post-processor: docker-import
    docker (docker-import): Importing image: Container
    docker (docker-import): Repository: test/test:0.1
    docker (docker-import): Imported ID: sha256:8421edb057bd9ca2bb8c33cba75979f0beb64476a957fb195c3d0b8b7927e67b
Build 'docker' finished.
```

## デバッグ方法
デフォルトではPacker内部でエラーが出るとはDockerのコンテナが削除されてしまい、原因を探ることができなってしまいます。
`-on-error=abort`オプションを追加すると、コンテナが削除されなくなりますので、`docker exec`で中に入って調査できるようになります。
```
$packer build -on-error=abort <ビルドファイル>.json
```

# Dockerfileと比べた利点
Dockerfileだと、各コマンドごとにイメージがコミットされていきます。

コミットされてしまったイメージは（後からその中のファイルを削除しても）小さくなることがないため、わざわざ

```
RUN apt-get update && \
    apt-get install <なんとか> && \
    apt-get remove <なんとか> && \
    rm -r <なんとか>
```

みたいに、&&で各コマンドを数珠つなぎにして、1つのステップとして実行するというバットノウハウを使う必要がありましたが、Packerではそのようなことを気にする必要がありません。


# 注意事項
Dockerコンテナを立ち上げるときにbashを起動します。
[Alpine Linux](https://alpinelinux.org)のようなデフォルトでbashが入っていないイメージを使う場合は`run_command`で代わりのシェル(`/bin/sh`)を指定する必要があります。

```json
    "builders":[{
        "type": "docker",
        "image": "alpine:edge",
        "export_path": "controller.tar",
        "run_command": ["-d", "-i", "-t", "{{.Image}}", "/bin/sh"]
    }]
```

# 参考
- [Pakcer ドキュメント](https://www.packer.io/docs/)