Packerを使ってAnsibleをDockerイメージにする

## 概要
Packerとは、簡単に言うと、マシンイメージを作成するオーケストレーションツールです。
Dockerイメージや、AWSのAMIを作成することもできます。
Vagrantなどを作っている、hashicorpのツールです。

https://www.packer.io/intro/index.html

今回は、このPackerを使って、AnsibleのPlaybookからDockerイメージを作成します。

## 仕組み
仕組みを簡単に説明すると、Packerでビルド環境を定義し、Packerが立ち上げたDockerコンテナでlocalモードでAnsibleを実行し、出来上がったコンテナをイメージ化するという仕組みになります。

## 環境
- CentOS 7.5
- packer 1.3.2

## packerのインストール
以下から環境にあったバイナリをダウンロードします。
https://www.packer.io/downloads.html

Linux環境なので、以下を使います。

```shell-session
$ curl -O https://releases.hashicorp.com/packer/1.3.2/packer_1.3.2_linux_amd64.zip
$ unzip packer_1.3.2_linux_amd64.zip
$ sudo cp packer /usr/local/bin
```

## ディレクトリ構成
後述のpacker.jsonの内容と合わせてご確認いただければと思いますが、こういうディレクトリ構成でpackerの環境を作ります。

```
.
├── packer.json
├── playbooks/
│   ├── group_vars/
│   ├── roles/
│   ├── site.yml
│   └── inventories
│       ├── group_vars/
│       └── hosts.sample
└── vault-password-file
```

## ビルド環境と内容を定義
ビルド環境と内容をjsonで定義します。
jsonファイルの名前は何でも構いませんが、ここではpackage.jsonとします。

```json:package.json
{
    "builders":[{
        "type": "docker",
        "image": "centos:7",
        "export_path": "controller.tar",
        "privileged": "true",
        "run_command": ["-d", "-i", "-t", "{{.Image}}", "/sbin/init"]
    }],
    
    "provisioners":[{
        "type": "shell",
        "inline": [
            "yum clean all",
            "yum -y install epel-release",
            "yum -y install PyYAML",
            "yum -y install python-jinja2",
            "yum -y install python-httplib2",
            "yum -y install python-paramiko",
            "yum -y install python-setuptools",
            "yum -y install ansible",
            "mkdir -p /tmp/ansible-local"
        ]
    }, {
        "type": "file",
        "source": "playbooks/",
        "destination": "/tmp/ansible-local"
    }, {
        "type": "file",
        "source": "vault-password-file",
        "destination": "/tmp/ansible-local/vault-password-file.txt"
    }, {
        "type": "ansible-local",
        "playbook_file": "playbooks/site.yml",
        "staging_directory": "/tmp/ansible-local",
        "extra_arguments": [
            "-i /tmp/ansible-local/inventories/hosts.sample",
            "--vault-password-file=/tmp/ansible-local/vault-password-file.txt"
        ],
        "clean_staging_directory": true
    }],

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

内容を簡単に説明します。

```shell:package.json
{
    # Ansibleを実行するDocker環境を定義します。docker-composeと大体同じです。
    "builders":[{
        "type": "docker",
        "image": "centos:7",
        "export_path": "controller.tar",
        "privileged": "true",
        "run_command": ["-d", "-i", "-t", "{{.Image}}", "/sbin/init"]
    }],
    # Ansibleを実行する環境を構築します。Dockerfileに当たる部分です。
    # Ansibleをインストールします。
    # また、作業ディレクトリとして/tmp/ansible-localを作成します。
    "provisioners":[{
        "type": "shell",
        "inline": [
            "yum clean all",
            "yum -y install epel-release",
            "yum -y install PyYAML",
            "yum -y install python-jinja2",
            "yum -y install python-httplib2",
            "yum -y install python-paramiko",
            "yum -y install python-setuptools",
            "yum -y install ansible",
            "mkdir -p /tmp/ansible-local"
        ]
    }, {
        # こちらは、ホストOSのファイルをコンテナ内にコピーする定義です。
        # Ansibleのファイルをすべてコピーします。
        "type": "file",
        # playbooks/{site.yml,roles,inventories,group_vars...}という構成だとします。
        "source": "playbooks/",
        # コンテナ内のコピー先ディレクトリです。
        "destination": "/tmp/ansible-local"
    }, {
        # vaultファイルを複合するパスワードが書かれたファイルです。
        "type": "file",
        # ホストOSで、複合パスワードを平文で書いたものを用意してください。
        "source": "vault-password-file.txt",
        "destination": "/tmp/ansible-local/vault-password-file.txt"
    }, {
        # Ansible実行時の引数などの定義です。
        # localモードで実行する場合は、ansible-local、sshの場合はansibleになります。
        "type": "ansible-local",
        # playbookのパスを指定します。
        "playbook_file": "playbooks/site.yml",
        # 以下にplaybookが設置されます。
        "staging_directory": "/tmp/ansible-local",
        "extra_arguments": [
            # localモードの場合、ansibleが勝手に-iオプションを付与しますが、それだけでは、inventories以下にあるgroup_varsが読み込まれません。
            # そのため、-iオプションが2つ付くことになりますが、以下でも定義します、。
            # また、inventory fileのパスを、「inventory_file」ディレクティブとして定義できますが、そうするとstaging_directory直下にコピーされ、意図した挙動になりません。
            "-i /tmp/ansible-local/inventories/hosts.sample",
            # vaultの複合パスワードが書かれたファイルを指定します。
            "--vault-password-file=/tmp/ansible-local/vault-password-file.txt"
        ],
        # ビルド完了後に、staging_directoryを削除するかどうかのフラグです。defaultはfalseです。
        "clean_staging_directory": true
    }],
    # 作成するDockerイメージの定義です。
    "post-processors": [{
        # Dockerイメージを作成し、ローカルに保存する。
        "type": "docker-import",
        # イメージ名
        "repository": "myname/image_name",
        # tag
        "tag": "0.1"
    }]
}
```

- その他のAnsible Provisionerのディレクティブは以下を参照
    - https://www.packer.io/docs/provisioners/ansible.html

### Ansible実行時の引数について
Dockerコンテナ内でroot権限でAnsibleを実行するので、sudoパスワードは不要です。
また、ansible-vaultの複合パスワードは、今回は`--vault-password-file`でファイルで渡すようにします。（というか、interactiveなやり方がわかりません。）


## ビルド実行
以下のコマンドを実行します。

```shell-session
$ sudo packer build -on-error=abort packer.json
```


そうすると、実際には以下のコマンドが実行されます。

```shell-session
$ cd /tmp/ansible-local && ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 \
    ansible-playbook /tmp/ansible-local/site.yml \
    --extra-vars "packer_build_name=docker packer_builder_type=docker packer_http_addr=" \
    -i /tmp/ansible-local/inventories/hosts.sample \
    --vault-password-file=/tmp/ansible-local/vault-password-file.txt \
    -c local -i /tmp/ansible-local/packer-provisioner-ansible-local714008969
```

特にエラーがでなければ成功です。

## Dockerイメージが作成されたか確認
ホストOS上にDockerイメージが作成されたことを確認します。

```shell-session
$ sudo docker images
REPOSITORY            TAG         IMAGE ID            CREATED            SIZE
myname/image_name     0.1         xxxxxxxxxxxx        1 minutes ago      846MB
```

## connection: docker
ansibleのplaybookに`connection: docker`という行を追加すると、docker containerへの接続となる。
https://hiyoko-infratech.hateblo.jp/entry/2017/12/31/191043
https://dev.classmethod.jp/server-side/os/ansible-docker-connection-plugin/

```yaml
- hosts: test_servers
  connection: docker
  tasks:
    - name: create directory
      file:
        path: /home/test/
        state: directory
        mode: 0755

    - name: create file
      file:
        path: /home/test/test.txt
        state: touch
        mode: 0644
```

コマンドにoptionを付ける場合

```
$ ansible-playbook -i "ansible-sync-test," reproduce.yml -c docker
```

## 参考
- https://www.packer.io/docs/provisioners/ansible.html
- [PackerとAnsibleでDocker imageを作る(Remote Ansible編)](https://blog.vtryo.me/entry/create-dockerimage-packer-remote-ansible)