Packer で開発環境の Vagrant Box を自作して、post-processors 処理を通して S3 に保存・バージョン管理・ホスティングする

サーバー上のシステムをチームで開発する場合、開発環境の構築をどう共有するかは課題になると思いますが、いくつかの既存のやり方には問題があります

# 今までの開発環境構築のやり方

### `README.md などに手順を書く方法`

これには次の問題が発生することがあります

- 本番環境（ex. Ubuntu）と開発環境（ex. Mac）で土台となる OS が違う
- 手順書の更新漏れがある
- 手順書の再現性の検証がおろそかになる
- 手順書の更新に対して、既存の開発環境をアップデートしないメンバーが出てくる

### `Vagrant を使う方法`

これは上記のいくつかを解消しますが、まだ問題が残ります

- 引き続き残る問題
 - 手順書の更新に対して、既存の開発環境をアップデートしないメンバーが出てくる
- 新たに発生した問題
 - `vagrant up` に時間がかかる（ex. 30 分以上とか）

`vagrant up` によって時間がかかる問題は、`apt-get` や `gem install` に失敗してやり直したりするリスク（時にはライブラリのインストールサーバーがしばらく不通になることもあると思います）も含めて、それなりにチーム全体でみると無駄な時間を作ってしまいます

# 開発環境構築済みの `Vagrant Box` を `S3` にホスティングするやり方

### 開発環境構築済みの `Vagrant Box` を作る

これによって、`vagrant up` に時間がかかる問題は解消します

- ライブラリのインストールに時間がかかったり、ライブラリをホストするサーバー依存で完了しないといったことがなくなります
- `Vagrant Box` のダウンロードがほとんどの時間を占めるので、ホスティング先や回線速度にもよりますが、5 ~ 10 分程度で終わるのではないでしょうか
 - ファイルサイズは、自分の感覚ではだいたい 300 MB ~ 1.2 GB 程度に収まることが多いのではと思います

### `Vagrant Box` を `manifest.json` と一緒に `S3` にホスティングする

これによって、既存の開発環境をアップデートしないメンバーが出てくるという問題は解消します

- 新しいバージョンの `Vagrant Box` をアップロードすると、[vagrant up 時にバージョンアップのチェックが自動実行されるので、古い場合には警告が表示されたり、バージョンアップするにはコマンド 1 発で出来るようになります](https://docs.vagrantup.com/v2/boxes/versioning.html)
 - `Vagrantfile` にて、バージョン制約もつけることができます

# `Vagrant Box` の生成と `S3` へのアップロードを行うために、`Packer` を使う

![packer_image.png](https://qiita-image-store.s3.amazonaws.com/0/24932/a471a2d2-45fc-417f-9136-a0771dd75268.png)

### `Packer` による `Vagrant Box` の生成

`Packer` を使うと、テンプレートファイルを用意することで `AMI` や `Vagrant Box` をある程度透過的に、作ることができます

- 例えば、`AWS` の `Auto Scaling` 機能を使う際に、インスタンスのセットアップをすぐに行うとか外部依存しないで終わらせるために、`AMI` を使うことは多いと思います
 - その `AMI` 構築をスクリプトで管理・実施することになるので、いわゆる、Infrastructure as a Code を実現することができます

すでに `AMI` 構築に `Packer` を使っている場合には、その構築手順を流用して `Vagrant Box` を作ることができます。これには、次のようなメリットがあります

- 本番環境の更新箇所と開発環境の更新箇所（= 要するにプロビジョニング）を共通化できる
- ソースコード上の位置的にも近接しているので、本番環境の構築スクリプト更新時に、開発環境の構築スクリプトの更新漏れが発見しやすい

まだ `Packer` を使っていない場合でも、後述するように、テンプレートファイルによる記述と実行の簡単さやプラグインによる拡張性の高さを理由に、`Packer` を使う方法をおすすめします

### `Packer` による `Vagrant Box` の `S3` へのアップロード

`Packer` はサードパーティ製のプラグインを使って、機能を拡張することができます

- [packer-post-processor-vagrant-s3](https://github.com/lmars/packer-post-processor-vagrant-s3) もそのひとつで、`Vagrant Box` の `S3` へのアップロードとそのバージョン管理をするための `manifest ファイル` の管理を自動化してくれます
- `post-processor` というのは、`Packer` が参照するテンプレートファイル内のブロックの名前のひとつです
 - ここでは、`Vagrant Box` を生成したメイン処理の後に、実施されるプラグインであるという意味になります

# 具体例をもとに、手順をまとめる

`Vagrant Box` それ自体を作成する作業と、そのプロビジョニング（= 必要なライブラリをインストールするなど）をする作業の 2 つに大別されます

### 前提

本番環境は `Ubuntu trusty` の AMI で動かしているとして、それに合わせる前提で考えます

また、`Packer` と `Vagrant` はすでにインストール済みで操作はわかっている前提で書きます

- これらの導入をまとめたチュートリアルはすでに多くあると思いますのでそれらを参照ください

### ubuntu-trusty-server-cloudimg-amd64 の OVA を作成する

`Vagrant Box`（≒ `VirtualBox Virtual Machines`）は、[ISO から作るか、OVF/OVA から作るかという 2 つの方法があります](https://www.packer.io/docs/builders/virtualbox.html)

- Qiita などには `ISO` から作るチュートリアルが多くあるのですが、スクラッチから作りたいわけではないならば、`OVF/OVA` から作るのがおすすめです
- 本番環境と同じような環境を作りたいだけなので、[公式に提供されているもの](https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box)から作った方が安全だと思うためです
 - [公式ドキュメント](https://www.packer.io/docs/builders/virtualbox.html)や[チュートリアルサイト](http://www.ipponusa.com/from-development-to-production-with-vagrant-and-packer/)にも同様の趣旨の記載があります
 - また、テンプレートファイル内に読んで理解しづらい（人によるでしょうが）ISO 生成の設定を書くのは避けたいと個人的には思っています

ubuntu 上で実行するなら[次の手順で作ることができます](https://github.com/flynn/flynn/tree/master/util/packer)

```
$ BOX_URL=https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box
$ curl $BOX_URL | tar --delete Vagrantfile > ubuntu.ova
```

mac 上では tar のオプションが違うので愚直な次の方法で作ることができました

```
$ BOX_URL=https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box
$ curl $BOX_URL > ubuntu.box
$ tar xvf ubuntu.box
$ rm Vagrantfile
$ tar cvf ubuntu_trusty.ova box.ovf box-disk1.vmdk 
```

### `Packer` のテンプレートファイルを作成する

これ以後、秘匿情報は `***` で隠してます

それぞれのブロックの意味

- `variables` では、ファイル中で変数として参照するための定義を書いてます
- `builders` では、`VirtualBox VM` を生成するための手順を書いています
- `provisioners` では、ライブラリのインストールなど、そのプロジェクト固有の設定をするための手順を書いています
- `post-processors` では、生成した `Vagrant Box` を `S3` にアップロードするための手順を書いています

アップロード先について

- `Vagrant Box` と `manifest.json` は S3 の `***-vagrant-box/vagrant` ディレクトリに配置されます

注意する点について

- `builders` の `vboxmanage` の `memory` を `2048` にしています
 - この値はプロジェクトによって必要最低限のメモリ使用量が良く、多ければマシンが重くなり、少なければプロジェクトが動きません
 - 例えば、`1024` の場合、自分のプロジェクトでは `OOM Kill` されました

```json:local.json
{
  "variables": {
      "version": "0.0.2",
      "aws_access_key": "***",
      "aws_secret_key": "***"
  },  
  "builders": [
    {
      "type": "virtualbox-ovf",
      "guest_additions_path": "VBoxGuestAdditions_{{.Version}}.iso",
      "headless": "true",
      "shutdown_command": "echo 'vagrant' | sudo -S shutdown -P now",
      "source_path": "ubuntu_trusty.ova",
      "ssh_password": "vagrant",
      "ssh_username": "vagrant",
      "ssh_wait_timeout": "20m",
      "vboxmanage": [
        ["modifyvm", "{{ .Name }}", "--memory", "2048"]
      ],
      "virtualbox_version_file": ".vbox_version",
      "vm_name": "mtburn-base"
    }
  ],
  "provisioners": [
      {
          "source": "files", 
          "destination": "/tmp/files",
          "type": "file"
      },
      {
          "scripts": [
              "scripts/ubuntu-trusty/basic.sh",
              "scripts/ubuntu-trusty/packages.sh",
              "scripts/ubuntu-trusty/user.sh",
              "scripts/ubuntu-trusty/perl.sh",
              "scripts/ubuntu-trusty/td-agent.sh",
              "scripts/ubuntu-trusty/mysql-server.sh",
              "scripts/ubuntu-trusty/redis-server.sh"
          ],
          "environment_vars": "MTBURN_ROLE=local",
          "type": "shell"
      }
  ],
  "post-processors": [
    [
        {
          "type": "vagrant", 
          "output": "/tmp/ubuntu-trusty-server-cloudimg-amd64.box"
        },
        {
          "type": "vagrant-s3",
          "region": "ap-northeast-1",
          "bucket": "***-vagrant-box",
          "manifest": "vagrant/manifest.json",
          "box_name": "mtburn-ubuntu-trusty-server-cloudimg-amd64",
          "box_dir": "vagrant/boxes",
          "version": "{{ user `version` }}",
          "access_key": "{{ user `aws_access_key` }}",
          "secret_key": "{{ user `aws_secret_key` }}"
        }
    ]
  ]
}
```

`packer build` します

- S3 のアップロードは回線状況などによって[失敗する場合があります](https://github.com/lmars/packer-post-processor-vagrant-s3/issues/2) 
 - そのため、足元にも生成した box を置くようにしてます
 - その場合には手動アップロードすることになりますが、手間がかかるので手順を後述します

```
$ packer build local.json
...
==> virtualbox-ovf: Running post-processor: vagrant
==> virtualbox-ovf (vagrant): Creating Vagrant box for 'virtualbox' provider
    virtualbox-ovf (vagrant): Copying from artifact: output-virtualbox-ovf/mtburn-base-disk1.vmdk
    virtualbox-ovf (vagrant): Copying from artifact: output-virtualbox-ovf/mtburn-base.ovf
    virtualbox-ovf (vagrant): Renaming the OVF to box.ovf...
    virtualbox-ovf (vagrant): Compressing: Vagrantfile
    virtualbox-ovf (vagrant): Compressing: box.ovf
    virtualbox-ovf (vagrant): Compressing: metadata.json
    virtualbox-ovf (vagrant): Compressing: mtburn-base-disk1.vmdk
==> virtualbox-ovf: Running post-processor: vagrant-s3
==> virtualbox-ovf (vagrant-s3): Preparing to upload box for 'virtualbox' provider to S3 bucket '***-vagrant-box'
    virtualbox-ovf (vagrant-s3): Box to upload: /tmp/ubuntu-trusty-server-cloudimg-amd64.box (1105662889 bytes)
    virtualbox-ovf (vagrant-s3): Fetching latest manifest
    virtualbox-ovf (vagrant-s3): Generating checksum
    virtualbox-ovf (vagrant-s3): Checksum is 4f44e9727719bb2b4780da33d40c27095eeffefcb4ad2ab68917bedecde87531
    virtualbox-ovf (vagrant-s3): Adding virtualbox 0.0.2 box to manifest
    virtualbox-ovf (vagrant-s3): Uploading box to S3: vagrant/boxes/0.0.2/ubuntu-trusty-server-cloudimg-amd64.box
    virtualbox-ovf (vagrant-s3): File size > 100MB. Initiating multipart upload
    virtualbox-ovf (vagrant-s3): Uploading the manifest: vagrant/manifest.json
Build 'virtualbox-ovf' finished.
```

# 上記の手順によって生成された成果物を確認する

`Vagrant Box` と `manifest.json` が `S3` 上の所定の位置にアップロードされているはずです

- 次の `manifest.json` は、理解のために、すでに `0.0.1` というバージョンをアップロードしたあとに、`0.0.2` という新しいバージョンをアップロードした場合のものです
 - これによって、これ以後新規に `vagrant up` すると、`0.0.2` の `Vagrant Box` をダウンロードして構築するようになります
 - また、すでに `0.0.1` を構築済みの環境で `vagrant up` した場合も、`vagrant box update` と打てば、`0.0.2` の `Vagrant Box` をダウンロードしてアップデートしてくれます

```json:manifest.json
{
  "name": "mtburn-ubuntu-trusty-server-cloudimg-amd64",
  "versions": [
    {
      "version": "0.0.1",
      "providers": [
        {
          "name": "virtualbox",
          "url": "https://s3-ap-northeast-1.amazonaws.com/***-vagrant-box/vagrant/boxes/0.0.1/ubuntu-trusty-server-cloudimg-amd64.box",
          "checksum_type": "sha256",
          "checksum": "d4cb23e3502c4717dff626ede4838e70ec26f92a495679ab57137f96fb7caade"
        }
      ]
    },
    {
      "version": "0.0.2",
      "providers": [
        {
          "name": "virtualbox",
          "url": "https://s3-ap-northeast-1.amazonaws.com/***-vagrant-box/vagrant/boxes/0.0.2/ubuntu-trusty-server-cloudimg-amd64.box",
          "checksum_type": "sha256",
          "checksum": "4f44e9727719bb2b4780da33d40c27095eeffefcb4ad2ab68917bedecde87531"
        }
      ]
    }
  ]
}
```

# 上記の手順によって生成された成果物を利用する

### `Vagrantfile` を用意する

`local_initfile` では `supervisord` などプロセス管理デーモンの起動など最低限の処理や、他の開発メンバーの任意に任される（= 不要なら削除・編集してもらって構わない）ような `/etc/hosts` の編集などを記述しています

- `Vagrantfile` と `local_initfile` は、プロジェクトのレポジトリに含めて管理・共有されます

```rb:Vagrantfile
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "mtburn-ubuntu-trusty-server-cloudimg-amd64"
  config.vm.box_url = "https://s3-ap-northeast-1.amazonaws.com/***-vagrant-box/vagrant/manifest.json"
  config.vm.provision "shell", path: "Packer/scripts/ubuntu-trusty/local_initfile"
  config.vm.network "private_network", ip: "172.20.20.10"
end
```

### `Vagrant Box` のバージョン管理

古いバージョンを使っている場合（すでに `S3` に `0.0.2` がありながら、手元はまだ `0.0.1` など）に、`vagrant up` すると次のような警告が出るようになります

```sh
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'mtburn-ubuntu-trusty-server-cloudimg-amd64' is up to date...
==> default: A newer version of the box 'mtburn-ubuntu-trusty-server-cloudimg-amd64' is available! You currently
==> default: have version '0.0.1'. The latest is version '0.0.2'. Run
==> default: `vagrant box update` to update.
==> default: VirtualBox VM is already running.
```

警告にある通り、`vagrant box update` を実行すれば最新に更新されます

```sh
$ vagrant box update
==> default: Checking for updates to 'mtburn-ubuntu-trusty-server-cloudimg-amd64'
    default: Latest installed version: 0.0.1
    default: Version constraints: 
    default: Provider: virtualbox
==> default: Updating 'mtburn-ubuntu-trusty-server-cloudimg-amd64' with provider 'virtualbox' from version
==> default: '0.0.1' to '0.0.2'...
==> default: Loading metadata for box 'https://s3-ap-northeast-1.amazonaws.com/***-vagrant-box/vagrant/manifest.json'
==> default: Adding box 'mtburn-ubuntu-trusty-server-cloudimg-amd64' (v0.0.2) for provider: virtualbox
...
```

# 補足

### `Vagrant Box` の手動アップロード

生成した `Vagrant Box` を `S3` にアップロードする `post-processors` 処理は回線状況によっては失敗する場合があります

- その場合、手動で `***-vagrant-packer/vagrant` 以下に `manifest.json`と `boxes/[version]/ubuntu-trusty-server-cloudimg-amd64.box` をアップロードしたい場合もあると思います

box はそのままアップロードすれば良いですが、`manifest.json` はすでに `S3` でホスティングされている内容に append する形式で新しいバージョンの情報を付け加える作業が必要になります

- この際の `sha256` の `checksum` は `openssl dgst -sha256 ubuntu-trusty-server-cloudimg-amd64.box` というコマンドで算出できます

また、`metadata` の `Content-type` が `application/json` になっていないといけない点に注意してください

### `AMI` を生成するテンプレートファイルの `post-processors` 処理内で `Vagrant Box` を生成する

例えば、すでに `AMI` を生成するために `Packer` を使っている場合、そのテンプレートファイルは次のような感じかと思います

```json:app.json
{   
    "variables": {
        "aws_access_key": "***",
        "aws_secret_key": "***"
    },
    "builders": [{
        "type": "amazon-ebs",
        "access_key": "{{user `aws_access_key`}}",
        "secret_key": "{{user `aws_secret_key`}}",
        "region": "ap-northeast-1",
        "source_ami": "ami-e74b60e6",
        "instance_type": "t2.micro",
        "ssh_username": "ubuntu",
        "ami_name": "app {{timestamp}}"
    }],
}
```

https://www.packer.io/intro/getting-started/vagrant.html を読むと、次のように `post-processors` で `type: "vagrant"` を指定すれば、`Vagrant Box` が生成されると書いてあります

```json:app-and-local.json
{   
    "variables": {
        "aws_access_key": "***",
        "aws_secret_key": "***"
    },
    "builders": [{
        "type": "amazon-ebs",
        "access_key": "{{user `aws_access_key`}}",
        "secret_key": "{{user `aws_secret_key`}}",
        "region": "ap-northeast-1",
        "source_ami": "ami-e74b60e6",
        "instance_type": "t2.micro",
        "ssh_username": "ubuntu",
        "ami_name": "app {{timestamp}}"
    }],
    "post-processors": [{
        "type": "vagrant",
        "output": "./Packer/box/ubuntu-trusty-14.04-amd64-server-for-local.box",
        "keep_input_artifact": true
    }]
}
```

これをビルドしてみると、確かに box ができます

- ただし、`Vagrant Box` の `provider` が `virtualbox` ではなく `aws` になっています
 - そのため、`.vmdk` が含まれず、[vagrant-aws plugin](https://github.com/mitchellh/vagrant-aws) を使う前提の box が生成されるだけです
 - `AMI` から生成されるので当たり前の結果といえばそうです

```sh
$ packer build Packer/app-and-local.json
amazon-ebs output will be in this color.

==> amazon-ebs: Inspecting the source AMI...
==> amazon-ebs: Creating temporary keypair: packer 54cdf714-446a-eef6-6415-8f7b2913bcf5
==> amazon-ebs: Creating temporary security group for this instance...
==> amazon-ebs: Authorizing SSH access on the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
    amazon-ebs: Instance ID: i-06b6ab1f
==> amazon-ebs: Waiting for instance (i-06b6ab1f) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Stopping the source instance...
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: app 1422784276
    amazon-ebs: AMI: ami-36e7ff37
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
==> amazon-ebs: Running post-processor: vagrant
==> amazon-ebs (vagrant): Creating Vagrant box for 'aws' provider
    amazon-ebs (vagrant): Compressing: Vagrantfile
    amazon-ebs (vagrant): Compressing: metadata.json
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:

ap-northeast-1: ami-36e7ff37
--> amazon-ebs: 'aws' provider box: ./Packer/box/ubuntu-trusty-14.04-amd64-server-for-local.box
$ ls -la Packer/box/
total 8
drwxr-xr-x  3 ***  staff  102  2  1 18:53 .
drwxr-xr-x  4 ***  staff  136  2  1 18:51 ..
-rw-r--r--  1 ***  staff  355  2  1 18:53 ubuntu-trusty-14.04-amd64-server-for-local.box
```

```sh
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
There are errors in the configuration of this machine. Please fix
the following errors and try again:

vm:
* The box 'local-mtburn-20150201' could not be found.
```

http://stackoverflow.com/questions/27953910/cannot-use-the-box-created-with-packer-post-processor-vagrant に同じ指摘が書いてあります

- http://smashingboxes.com/ideas/how-to-convert-ec2-ami-to-vmdk-for-vagrant に記載されているような変換処理を内部でしてくれるわけではありませんでした
- この結果を受けて、`ova` から作る最初からの手順によって構築することに決めました