Packerを使って1コマンドで本番サーバと開発サーバ (のVMイメージ)を作る話

> この記事は[VASILY DEVELOPERS BLOG](http://tech.vasily.jp/entry/create-prod-and-dev-vm-image-atst-by-packer)にも同じ内容で投稿しています。よろしければ他の記事もご覧ください。
> 

# Qiita追記
2016.07.21に**Ubuntu16.04.1**が公開されました。各プラットフォームのベースイメージにも用意されています。
本文中で参照しているイメージのOSは**Ubuntu16.04**と少し古いため、適宜IDを更新して下さい。

本文のjsonをそのまま使ってエラーが出た場合は、ベースとなるVMイメージが各プラットフォームに存在しているかご確認下さい。

# 本文

開発をしていると本番サーバと開発サーバの乖離が問題になると思います。これについて、先日行われた[UZABASE Meetup#4 〜大規模サービスを支えるインフラ〜](http://uzabase-meetup.connpass.com/event/33709/)にて「1コマンドで本番サーバと開発サーバ (のVMイメージ)を作る話」という発表をさせていただきました。

<script async class="speakerdeck-embed" data-id="3f0048e00cb2495ab5920b139bd6547a" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

この記事では、時間とスライドの都合上、省略したbase.jsonについてご紹介いたします。

<!-- more -->

# packer build base.json
[発表資料はこちら（speederdeck）](https://speakerdeck.com/kotatsu360/create-prod-and-dev-vm-image-atst-by-packer)

packerで読み込むjsonは次の4パートに分かれています。

```json
  "variables":{
  // 変数
  }, 
  "builders":[
  // 作成したいプラットフォームごとの設定
  ],
  "provisioners": [
  // マシンイメージへの初期設定 chef, shell script, ansible ...
  ],
  "post-processors": [
  // 作成したマシンイメージへの後処理
  ]
```

非常にシンプルなのですが、実際に設定していくと細かなパラメータの設定で悩みます。ということで実際に使っているjsonファイル全文をこちらにご用意しました！

packer/base.json

```json
{
  "variables":{
    "version":  "1.0.2",
    "role": "base",
    "ami": "ami-5d38d93c",
    "aws_access": "{{ env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret": "{{ env `AWS_SECRET_ACCESS_KEY`}}",
    "gce_source_image": "ubuntu-1604-xenial-v20160627",
    "gce_secret": "{{ env `GCE_ACCOUNT_FILE`}}",
    "s3_bucket": "{{ env `AWS_INFRA_S3_BUCKET`}}",
    "gce_project_id": "{{ env `GCE_PROJECT_ID`}}"
  },
  "builders":[
    {
      "type": "virtualbox-ovf",
      "headless": "true",
      "shutdown_command": "echo 'ubuntu' | sudo -S shutdown -P now",
      "source_path": "box-source/ubuntu-16.04.ova",
      "ssh_password": "ubuntu",
      "ssh_username": "ubuntu",
      "ssh_wait_timeout": "20m",
      "vboxmanage": [
        ["modifyvm", "{{ .Name }}", "--memory", "4096"],
        ["modifyvm", "{{ .Name }}", "--cpus", "2"]
      ],
      "virtualbox_version_file": ".vbox_version",
      "vm_name": "{{user `role`}}",
      "guest_additions_mode": "disable",
      "format": "ova",
      "output_directory": "output-{{build_name}}-{{user `role`}}"
    },
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access`}}",
      "secret_key": "{{user `aws_secret`}}",
      "source_ami": "{{user `ami`}}",
      "instance_type": "c3.xlarge",
      "region": "ap-northeast-1",
      "ssh_username": "ubuntu",

      "ami_name": "packer-ubuntu1604-ruby231-{{timestamp}}",
      "ami_regions": [
        "ap-northeast-1"
      ],
      "ami_description" : "iQON AMI {{user `role`}} Image",
      "tags": {
        "OS": "Ubuntu16.04",
        "Ruby": "2.3.1",
        "Role": "{{user `role`}}",
        "OriginalAMI": "ami-5d38d93c"
      }
    },
    {
      "type": "googlecompute",
      "account_file": "{{user `gce_secret`}}",
      "project_id": "{{user `gce_project_id`}}",
      "source_image": "{{user `gce_source_image`}}",
      "zone": "asia-east1-a",
      "machine_type": "n1-highcpu-4",
      "ssh_username": "ubuntu",
      "instance_name": "packer-{{timestamp}}",
      "image_name": "packer-ubuntu1604-ruby231-{{timestamp}}",
      "image_description" : "iQON AMI {{user `Role`}} Image"

    }
  ],
  "provisioners": [
    {
      "type": "file",
      "source": "{{pwd}}/../chef-repo",
      "destination": "/tmp/packer-chef-client/"
    },
    {
      "type": "shell",
      "inline": [
        "sudo apt-get update",
        "sudo apt-get upgrade -y",
        "sudo apt-get install -y language-pack-ja curl",
        "sudo update-locale LANG=ja_JP.UTF-8 && true",
        "sudo ln -sf /bin/bash /bin/sh"
      ]
    },
    {
      "type": "chef-client",
      "server_url": "http://localhost:8889",
      "config_template": "../chef-repo/client.rb",
      "install_command": "curl -L https://www.chef.io/chef/install.sh | sudo bash  -s -- -v 12.8.1",
      "execute_command": "sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json",
      "guest_os_type": "unix",
      "skip_clean_node": true,
      "skip_clean_client": true
    },
    {
      "type": "shell",
      "only": ["virtualbox-ovf"],
      "inline": [
        "sudo systemctl disable apt-daily.service",
        "sudo systemctl disable apt-daily.timer"
      ]
    }
  ],
  "post-processors": [
    {
      "type": "shell-local",
      "only": ["virtualbox-ovf"],
      "inline": [
        "rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}}",
        "aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source"
      ]
    },
    [
      {
        "type": "vagrant",
        "only": ["virtualbox-ovf"],
        "keep_input_artifact": false,
        "output": "packer-output/{{user `role`}}/{{user `role`}}.box",
        "override": {
          "virtualbox": {
            "compression_level": 0
          }
        }
      },
      {
        "type": "vagrant-s3",
        "only": ["virtualbox-ovf"],
        "region": "ap-northeast-1",
        "bucket":   "{{user `s3_bucket`}}",
        "manifest": "vagrant/json/{{user `role`}}.json",
        "box_name": "{{user `role`}}",
        "box_dir":  "vagrant/boxes",
        "version":  "{{ user `version` }}",
        "acl": "private",
        "access_key_id": "{{user `aws_access`}}",
        "secret_key": "{{user `aws_secret`}}"
      }
    ]
  ]
}

```

シークレットや組織固有の部分については環境変数を読み込むようにしています。また実行時に引数として渡す事も可能です。

[User Variables in Templates - Packer by HashiCorp](https://www.packer.io/docs/templates/user-variables.html)

# base.jsonの中身

packerはinspectというサブコマンドでそのJSONで何が実行されるのかが確認できます。base.jsonを見てみます。

```bash
$ packer inspect base.json
Optional variables and their defaults:

  ami              = ami-5d38d93c
  aws_access       = {{ env `AWS_ACCESS_KEY_ID`}}
  aws_secret       = {{ env `AWS_SECRET_ACCESS_KEY`}}
  gce_project_id   = {{ env `GCE_PROJECT_ID`}}
  gce_secret       = {{ env `GCE_ACCOUNT_FILE`}}
  gce_source_image = ubuntu-1604-xenial-v20160627
  role             = base
  s3_bucket        = {{ env `AWS_INFRA_S3_BUCKET`}}
  version          = 1.0.2

Builders:

  amazon-ebs    
  googlecompute 
  virtualbox-ovf

Provisioners:

  file
  shell
  chef-client
  shell
```

このbase.jsonでは、9個のユーザ定義変数で動作が制御されており、

* ami (EBS-attached)
* google compute engine image
* virtualbox ovf (vagrant box用）

が作られ、それぞれプロビジョンは

1. file copy
2. remote shellの実行
3. chef client
4. remote shellの実行

という順番で行われる。ということが分かります。もう少し分解して見ていきます。

## variables
AWSのトークンやオリジナルAMI (ここではUbuntu16.04) を設定します。
複数回登場する要素はvariablesで定義しておいた方が見通しが良くなります。

シークレットをファイルに含めなくて済むのでGitHubにコミットするときも安全です。

```json
  "variables":{
    "aws_access": "{{ env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret": "{{ env `AWS_SECRET_ACCESS_KEY`}}"
  },
```

## builders
Vagrant boxの元となるVirtualBoxと本番で使うAMI/GCE Imageを作っています。

```json
  "builders":[
    {
      "type": "virtualbox-ovf",
      "source_path": "box-source/ubuntu-16.04.ova",
      ...
    },
    {
      "type": "amazon-ebs",
      ...
    },
    {
      "type": "googlecompute",
      ...
  ]
```

AMI/GCE Imageについては見たままです。各プラットフォームが公式で提供しているUbuntu16.04のイメージを使ってインスタンスを立て、後述のプロビジョニングを行い、マシンイメージを保存してインスタンスを削除してくれます。

一方、VirtualBoxについては一手間かけています。`source_path`で指定しているovaは[Canonicalが提供しているiso](http://releases.ubuntu.com/16.04/)を一度VirtualBoxに入れて、OVF2.0でエクスポートしたものです。

Ubuntu14.04だと[Canonical公式のbox](https://cloud-images.ubuntu.com/vagrant/trusty/current/)をtarで解凍したときに出てくるovaをpackerで読み込めたのですが、[Ubuntu16.04のbox](https://cloud-images.ubuntu.com/xenial/current/)から同様に作成したovaは読み込みエラーになったため自分で作成しました。


## provisioners
```json
  "provisioners": [
    {
      "type": "file",
      "source": "{{pwd}}/../chef-repo",
      "destination": "/tmp/packer-chef-client/"
    },
    {
      "type": "shell",
      "inline": [
        "sudo apt-get update",
        "sudo apt-get upgrade -y",
        "sudo apt-get install -y language-pack-ja curl",
        "sudo update-locale LANG=ja_JP.UTF-8 && true",
        "sudo ln -sf /bin/bash /bin/sh"
      ]
    },
    {
      "type": "chef-client",
      "config_template": "../chef-repo/client.rb",
      "execute_command": "sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json",
    },
    {
      "type": "shell",
      "only": ["virtualbox-ovf"],
      "inline": [
        "sudo systemctl disable apt-daily.service",
        "sudo systemctl disable apt-daily.timer"
      ]
    }
  ],
```

packerで一番ハマったのがこのprovisinersです。大きな流れは、

1. chefで使うファイルをローカルからVMにコピー
2. 設定をしておかないとそもそもchefが実行できない処理をremote shellで実行
3. chef clientをlocal modeで実行
4. `virtualbox-ovf`限定で、インスタンス起動時のapt-get updateを停止

ということをやっています。4番目は発表資料の23ページ目で触れているapt-getがchefと衝突するのを避けるためです。
<script async class="speakerdeck-embed" data-slide="23" data-id="3f0048e00cb2495ab5920b139bd6547a" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

ちなみに、3で参照しているclient.rbの中身はこうなっています。1でコピーしたchef-repoの構造を指示しています。

```ruby
# coding: utf-8
chef_repo_path "/tmp/packer-chef-client"
cookbook_path [
  "/tmp/packer-chef-client/site-cookbooks",
  "/tmp/packer-chef-client/cookbooks"
]
log_location "/var/log/chef-client.log"
log_level :info
```

## post-processors
```json
  "post-processors": [
    {
      "type": "shell-local",
      "only": ["virtualbox-ovf"],
      "inline": [
        "rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}}",
        "aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source"
      ]
    },
    [
      {
        "type": "vagrant",
        "only": ["virtualbox-ovf"],
        ...
      },
      {
        "type": "vagrant-s3",
        "only": ["virtualbox-ovf"],
        "manifest": "vagrant/json/{{user `role`}}.json",
        ...
      }
    ]
  ]
```

post-processorsはbuildersで作成したVMイメージに対して後処理を行うことができます。ここでは、`virtualbox-ovf`の結果を使って、vagrant boxを作成しています。

できたものはS3に保存し、チーム全員で共通のboxを使えるようにしています。この管理方法についてはこちらの投稿を参考にさせていただきました。

[Packer で開発環境の Vagrant Box を自作して、post-processors 処理を通して S3 に保存・バージョン管理・ホスティングする - Qiita](http://qiita.com/yoheimuta/items/fa4c7051940fee09c08e)

# その他補足
## トークンの権限について
AWS/GCEのトークンに必要な権限については、公式ドキュメントで丁寧に紹介されていますので上では説明を省略しています。

[https://www.packer.io/docs/builders/amazon.html:title]

[https://www.packer.io/docs/builders/googlecompute.html:title]

## ビルド対象について
```bash
packer build base.json
```
と実行すると3つのプラットフォームでビルドが始まりますが、対象を絞ることもできます。

```bash
packer build -only='amazon-ebs' base.json
```
この場合、AMIだけビルドが始まります。

[https://www.packer.io/docs/command-line/build.html:title]

## AWSのインスタンスタイプについて
packerというよりもAWSの話題になりますが、検証中Ubuntu16.04 + {m,c,r}3.largeのインスタンスの場合にカーネルパニックが発生し正常に起動しないという問題がありました。

こちらのissueに近いのですが詳細が確認できず、largeを使わないという対応策を取っています。

[https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1573231:title]

もう直っているかもしれませんがお気をつけ下さい。

# 現状の制約
このbase.jsonを使う際、2つ解決できてない問題があります。

## 一つ目：`virtualbox-ovf`の一時ファイル
packerは一時ファイルが残っていると実行時にエラーが発生します。
基本動作としては消えるはずなのですが、post-provisionersの書き方が悪いのか、ある時から消えなくなってしまいました。

```bash
# 2回目の実行
$ packer build base.json
virtualbox-ovf output will be in this color.

Build 'virtualbox-ovf' errored: Output directory exists: output-virtualbox-ovf-base

Use the force flag to delete it prior to building.
```

手動でディレクトリを消すか、`-force`を付けてbuildを実行して下さい。

```bash
packer build -force base.json
```

## 二つ目：バージョンの手動変更
vagrant boxの管理でmanifest.jsonを内部で生成しており、既に存在するバージョンの場合は最後の`vagrant-s3`が失敗します。

```json
  "variables":{
    "version":  "1.0.2",
```

vagrantの仕様上、`x.x.x`の形式にする必要があり、タイムスタンプというわけにもいきません。人間インクリメントなのでなんとかしたいと思っています。

## 今後やりたいこと
まだまだこなれておらず、日々jsonを更新しています。
例えば、EC2に関してはスポットインスタンスを使うよう修正をしているところです。
ハイパフォーマンスのインスタンスが手頃な値段で使えるため、より快適なイメージ更新作業ができると思っています。

# まとめ
かなり駆け足ではありましたが、実際に運用しているpacker用のJSONファイルについてご紹介いたしました。
packerは簡単に始められますが、ちょっと凝ったことをしようと思うとやはりオプションの調整が避けられません。

この記事が何かしら参考になれば幸いです。

逆に「お前のpacker術は間違っている」という部分がありましたら、コメントでご指摘下さい。小躍りして喜びます。