PackerのAnsibleProvisionerでインベントリ毎のgroup_varsを使い分ける

# はじめに

この記事は、エーピーコミュニケーションズ Advent Calendar 2018 の22日目のエントリです。

今回もAnsible絡みですが、Packerを使ってAWS EC2にAMIをデプロイします。

PackerにはProvisionerというVM起動後にインストール作業や設定変更ができる機能があり、
その中に[Ansible Provisioner](https://www.packer.io/docs/provisioners/ansible.html)があるのでそちらを使います。
PackerのProvisionerにはAnsibleの他にもShellやPowerShell, Chef, Puppetなどなど各種あるのでお好みのツールを使えると思います。
ちなみにもう一つ[Ansible Local Provisioner](https://www.packer.io/docs/provisioners/ansible-local.html)というものがありますが、そちらは試していません。

# やりたいこと

Packerは基本的に１台のマシンイメージ（AMI）を作成するツールですが、
Ansibleは複数のマシンをホスト名やグループ名によってインストールするパッケージなどの処理を変えながら構築していくことを多いと思われます。
こういった使い方をPackerで実現するため、所属するグループをサーバの役割毎に動的に変更させることによって複数種類のAMIを作成させます。

また、Packerではインベントリを使用せずにPlaybookのみで動作させることができます。
クラウドでは基本的にIPやホスト名が動的に割り当てられるため、あらかじめインベントリを作成しておくのが困難なので、作成する必要がないのは楽ですね。
インベントリを使用しなくてもPlaybook側の`group_vars`を使用すれば上記の挙動は実現可能ですが、
Ansible Bestpracticeにある[Alternative Directory Layout](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#alternative-directory-layout)の例のように
「商用とステージングでPlaybookとグループは共通のものを使いたいけど、IPアドレスやURLなどの変数には別々の値を使いたい」という場合には
複数のインベントリファイル・ディレクトリに分けて使い分ける、という使い方をしているケースも多く、
その場合にはインベントリの`host_vars`, `group_vars`を使い分ける必要があります。

<details><summary>インベントリのディレクトリ構成例（Best Practicesから抜粋）</summary><div>

```yaml
inventories/
   production/
      hosts               # inventory file for production servers
      group_vars/
         group1.yml       # here we assign variables to particular groups
         group2.yml
      host_vars/
         hostname1.yml    # here we assign variables to particular systems
         hostname2.yml

   staging/
      hosts               # inventory file for staging environment
      group_vars/
         group1.yml       # here we assign variables to particular groups
         group2.yml
      host_vars/
         stagehost1.yml   # here we assign variables to particular systems
         stagehost2.yml
```
</div></details>
Packerでインベントリの`group_vars`を使う良い方法が調べていたのですが、
意外にもあまり情報が見つからなかったのでPackerの公式などを調べて動かした結果を書きます。

# 書き方

## ファイル構成

packerコマンドを実行するディレクトリからの相対的なファイル構成は以下のようにしました。

```sh:ファイル構成
.
├── ansible
│   ├── group_vars
│   │   ├── all.yml
│   │   ├── db.yml
│   │   └── web.yml
│   ├── inventory
│   │   ├── production
│   │   │   └── group_vars
│   │   │       └── all.yml
│   │   └── staging
│   │       └── group_vars
│   │           └── all.yml
│   └── play.yml
└── packer.json
```

`packer.json`がpackerコマンドで引数となる設定ファイルで、今回は実行ディレクトリ直下においています。
そして`ansible/inventory/production`および`ansible/inventory/staging`がインベントリに当たるディレクトリですが、
ポイントとしては、**インベントリファイルは作成しない**で、`group_vars`のみ（必要なら`host_vars`も）を置いている点です。

そしてPackerの設定ファイルは以下のように書きました。

```json:packer.json
{
    "variables": {
        "environment": "staging",
        "instance_type": "t2.micro",
        "source_ami": "ami-0a2de1c3b415889d2",
        "role": ""
    },
    "builders": [
        {
            "type": "amazon-ebs",
            "ami_name": "packer_ami_{{timestamp}}",
            "instance_type": "{{user `instance_type`}}",
            "source_ami": "{{user `source_ami`}}",
            "ssh_username": "ec2-user",
            "tags": [
                {
                    "Name": "packer_ami"
                }
            ]
        }
    ],
    "provisioners": [
        {
            "playbook_file": "ansible/play.yml",
            "inventory_directory": "ansible/inventory/{{user `environment`}}",
            "type": "ansible",
            "user": "ec2-user",
            "groups": ["{{user `role`}}"],
            "extra_arguments": []
        }
    ]
}
```

ポイントとしては
`provisioners`のディレクティブで[inventory_directory](https://www.packer.io/docs/provisioners/ansible.html#inventory_directory)を指定して、
その上で[groups](https://www.packer.io/docs/provisioners/ansible.html#groups)にグループ名を指定する
ということです。

それでは実際に動作確認してみましょう。
Playbookは以下のようにしています。

```yaml:ansible/play.yml
- hosts: all
  become: true
  tasks:
  - name: インベントリのgroup_vars/all.ymlの確認
    debug:
      msg: "my environment is {{ env }}"

  - name: プレイブックのgroup_vars/all.ymlの確認
    yum:
      name: "{{ common_package }}"

  - name: プレイブックのgroup毎のgroup_varsの確認
    yum:
      name: "{{ item }}"
    loop: "{{ dist_package }}"

- hosts: web
  tasks:
  - name: webグループの挙動確認
    debug:
      msg: "I am Web!!!"

- hosts: db
  tasks:
  - name: dbグループの挙動確認
    debug:
      msg: "I am db!!!"
```

各種ファイルに書いた変数はこちら

```yaml
::::::::::::::
ansible/group_vars/all.yml
::::::::::::::
common_package: git
::::::::::::::
ansible/group_vars/db.yml
::::::::::::::
dist_package:
- mysql
- mariadb-server
::::::::::::::
ansible/group_vars/web.yml
::::::::::::::
dist_package:
- httpd
- php
::::::::::::::
ansible/inventory/production/group_vars/all.yml
::::::::::::::
env: production
::::::::::::::
ansible/inventory/staging/group_vars/all.yml
::::::::::::::
env: staging
```

Packerでは`-var arg=var`という形式で`variables`で指定した変数を上書きできます。（`--var`ではなくハイフン１つなので注意）
実行結果は以下のようになりました。（Packerのメタな部分は適度にボカしてます）

```shell:実行結果
$ packer build -var environment=production -var role=web packer.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: packer_ami_1545451854
    amazon-ebs: Found Image ID: ami-0a2de1c3b415889d2
==> amazon-ebs: Creating temporary keypair: packer_***************
==> amazon-ebs: Creating temporary security group for this instance: packer_***************
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-***************
==> amazon-ebs: Waiting for instance (i-***************) to become ready...
==> amazon-ebs: Using ssh communicator to connect: ***************
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with Ansible...
==> amazon-ebs: Executing Ansible: ansible-playbook --extra-vars packer_build_name=amazon-ebs packer_builder_type=amazon-ebs -i ansible/inventory/production /home/hogehoge/code/packer-ansible/ansible/play.yml -e ansible_ssh_private_key_file=/tmp/ansible-key657870806
    amazon-ebs:
    amazon-ebs: PLAY [all] *********************************************************************
    amazon-ebs:
    amazon-ebs: TASK [Gathering Facts] *********************************************************
    amazon-ebs: ok: [default]
    amazon-ebs:
    amazon-ebs: TASK [インベントリのgroup_vars/all.ymlの確認] ********************************************
    amazon-ebs: ok: [default] => {
    amazon-ebs:     "msg": "my environment is production"
    amazon-ebs: }
    amazon-ebs:
    amazon-ebs: TASK [プレイブックのgroup_vars/all.ymlの確認] ********************************************
    amazon-ebs: changed: [default]
    amazon-ebs:
    amazon-ebs: TASK [プレイブックのgroup毎のgroup_varsの確認] *********************************************
    amazon-ebs: changed: [default] => (item=httpd)
    amazon-ebs: changed: [default] => (item=php)
    amazon-ebs:
    amazon-ebs: PLAY [web] *********************************************************************
    amazon-ebs:
    amazon-ebs: TASK [Gathering Facts] *********************************************************
    amazon-ebs: ok: [default]
    amazon-ebs:
    amazon-ebs: TASK [webグループの挙動確認] ************************************************************
    amazon-ebs: ok: [default] => {
    amazon-ebs:     "msg": "I am Web!!!"
    amazon-ebs: }
    amazon-ebs:  [WARNING]: Could not match supplied host pattern, ignoring: db
    amazon-ebs:
    amazon-ebs: PLAY [db] **********************************************************************
    amazon-ebs: skipping: no hosts matched
    amazon-ebs:
    amazon-ebs: PLAY RECAP *********************************************************************
    amazon-ebs: default                    : ok=6    changed=2    unreachable=0    failed=0
    amazon-ebs:
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating unencrypted AMI packer_ami_1545451854 from instance i-***************
    amazon-ebs: AMI: ami-***************
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Adding tags to AMI (ami-***************)...
==> amazon-ebs: Tagging snapshot: snap-***************
==> amazon-ebs: Creating AMI tags
    amazon-ebs: Adding tag: "Name": "packer_ami"
==> amazon-ebs: Creating snapshot tags
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-***************
```

想定した通りになってますね！

ちなみに[inventory_directory](https://www.packer.io/docs/provisioners/ansible.html#inventory_directory)の説明にもあるように、
inventory_directoryに指定したディレクトリに一時ファイルとしてインベントリファイルを作成します。
一時ファイルなので完了/失敗時に削除されるのですが、`Ctrl+C`を連打したりすると削除処理までキャンセルしてしまう場合がありました。
その場合、どういう挙動なのかよくわかってませんが、次回実行時に残ったインベントリファイルを読み込んでデプロイにコケる、というケースがありました。
挙動がおかしい場合はインベントリディレクトリを調査して、インベントリファイルが残ってたら削除するとうまくいきました。

他にもいい方法があればコメントください。