Packer + chef-solo + RakeでVagrant用のboxを作ってS3にアップロードする

# はじめに
以前、[Packer + chef-soloでカスタムAMI(AmazonMachineImage)を作る]()という記事を書きましたが、今回はVagrant用のbox(VirtualBox用)を作ります。でもって、ローカルで持ってても自分しか使えないので、S3にboxファイルをアップロードします。

[Packer](https://www.packer.io/)は非常にシンプルなJSONの定義で動くのですが、いきなり0から作ろうと思うと、フィードバックループに時間もかかるしそれなりに大変なので、Chef社から提供されている[bento](https://github.com/opscode/bento)をsubmoduleとし、かつtemplateをカスタムする形にしました。

bentoの他にも、いくつかテンプレートを提供してくれている方々がいらっしゃるのでいくつかご紹介します。

* https://github.com/box-cutter
* https://github.com/misheska/basebox-packer
* https://github.com/shiguredo/packer-templates
* https://github.com/hnakamur/my-packer-template-files
* https://github.com/nickchappell/packer-templates

また、Packerコマンドの実行とS3へのアップロードはRakeで行います。Go言語が得意な方は[Packerのプラグイン](https://www.packer.io/docs/extend/developing-plugins.html)を自作したほうがスマートかもしれません。



# 前提条件
* Packerインストール済み
* VirtualBoxインストール済み
* AWSアカウント作成済み
* S3にアップロード権限のあるIDとKEYを発行済み
* Chef導入済み
* chef-rbenvインストール済み

# ゴール
* Ruby2.1.5をビルドしたVagrant用のBoxをS3にアップロードする



# 実施
### Gemfileを用意する
```ruby:Gemfile
source "https://rubygems.org"

gem "rake"
gem "dotenv"
gem "aws-sdk-core"
```

### Gemをインストールする
```bash
$ bundle install --path vendor/bundle
```

### bentoの置き場を作る
```bash
$ mkdir vendor/templates
```

### bentoをsubmoduleとする
```bash
$ cd vendor/template
$ git submodule add https://github.com/opscode/bento.git bento
```

### bentoのモジュールにシンボリックリンクを貼る
```bash
$ ln -s vendor/templates/bento/packer/vagrantfile_templates vagrantfile_templates
$ ln -s vendor/templates/bento/packer/scripts scripts
$ ln -s vendor/templates/bento/packer/http http
$ ln -s vendor/templates/bento/packer/floppy floppy
```

### テンプレートファイルを用意する
```json:template.json
{
  "variables": {
    "centos_version": "6.6",
    "chef_version": "11.16.4",
    "mirror": "http://ftp.iij.ad.jp/pub/linux/centos",
    "ruby_version": "2.1.5",
    "box_output_dir": ""
  },
  "builders": [
    {
      "type": "virtualbox-iso",
      "boot_command": [
        "<tab> text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/centos-{{user `centos_version`}}/ks.cfg<enter><wait>"
      ],
      "boot_wait": "10s",
      "disk_size": 40960,
      "guest_additions_path": "VBoxGuestAdditions_{{.Version}}.iso",
      "guest_os_type": "RedHat_64",
      "http_directory": "http",
      "iso_checksum": "08be09fd7276822bd3468af8f96198279ffc41f0",
      "iso_checksum_type": "sha1",
      "iso_url": "{{user `mirror`}}/{{user `centos_version`}}/isos/x86_64/CentOS-{{user `centos_version`}}-x86_64-bin-DVD1.iso",
      "output_directory": "packer-centos-{{user `centos_version`}}-x86_64-virtualbox",
      "shutdown_command": "echo 'vagrant'|sudo -S /sbin/halt -h -p",
      "ssh_password": "vagrant",
      "ssh_port": 22,
      "ssh_username": "vagrant",
      "ssh_wait_timeout": "10000s",
      "vboxmanage": [
        [
          "modifyvm",
          "{{.Name}}",
          "--memory",
          "480"
        ],
        [
          "modifyvm",
          "{{.Name}}",
          "--cpus",
          "1"
        ]
      ],
      "virtualbox_version_file": ".vbox_version",
      "vm_name": "packer-centos-{{user `centos_version`}}-x86_64"
    }
  ],
  "post-processors": [
    {
      "type": "vagrant",
      "only": ["virtualbox-iso"],
      "output": "{{user `box_output_dir`}}/centos-{{user `centos_version`}}-x86_64-ruby-{{user `ruby_version`}}-{{timestamp}}.box"
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "only": ["virtualbox-iso"],
      "environment_vars": [
        "CHEF_VERSION={{user `chef_version`}}"
      ],
      "execute_command": "echo 'vagrant' | {{.Vars}} sudo -S -E bash '{{.Path}}'",
      "scripts": [
        "scripts/centos/fix-slow-dns.sh",
        "scripts/common/sshd.sh",
        "scripts/common/vagrant.sh",
        "scripts/common/vmtools.sh",
        "scripts/common/chef.sh",
        "scripts/centos/cleanup.sh",
        "scripts/common/minimize.sh"
      ]
    },
    {
      "type": "chef-solo",
      "install_command": "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo{{end}} bash -s -- -v {{user `chef_version`}}",
      "cookbook_paths": ["/path/to/chef/cookbooks"],
      "run_list": ["ruby_build", "rbenv::system"],
      "json": {
        "rbenv" : {
          "global": "{{user `ruby_version`}}",
          "rubies": [
            "{{user `ruby_version`}}"
          ]
        }
      },
      "prevent_sudo": false
    },
    {
      "type": "shell",
      "inline": [
        "sudo chmod -R 775 /usr/local/rbenv/"
      ]
    }
  ]
}
```

### S3へのアップロードに必要な環境変数を定義する
```ruby:.env
AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
S3_BUCKET = "S3_BUCKET"
REGION = "ap-northeast-1"
```

### Rakeファイルを用意する [^1]
[^1]: キーワード引数を利用しているため、Ruby2.1以降でないとエラーとなります

```ruby:Rakefile
require "rake"
require "aws-sdk-core"
require "dotenv"

Dotenv.load

# テンプレートとするコンフィグ
TEMPLATE_JSON = "template.json"

# 設定ファイルに渡せる変数
VARIABLES = %w[
  chef_version
  centos_version
  ruby_version
]

# box出力用ディレクトリ
BOX_OUTPUT_DIR = "./build"

namespace :packer do
  namespace :vb do
    desc "Run packer validate for virtualbox-iso"
    task :validate do
      exec_packer(
        builder_type: "virtualbox-iso",
        sub_command: "validate"
      )
    end

    desc "Run packer build for virtualbox-iso"
    task :build do
      exec_packer(
        builder_type: "virtualbox-iso",
        sub_command: "build"
      )

      upload_box_to_s3
    end

    desc "Run s3 upload for virtualbox-iso"
    task :s3upload do
      upload_box_to_s3
    end
  end

  # packerコマンドを実行します
  # - builder_type ビルドタイプ String
  # - sub_command サブコマンド String
  #
  # Returns 実行したコマンド(debug用)
  def exec_packer(builder_type:, sub_command:)
    if Dir.exist?(BOX_OUTPUT_DIR)
      # 念のためboxの出力先をクリーンしておく
      Dir.glob("#{BOX_OUTPUT_DIR}/*").each { |f| File.unlink(f) }
    else
      Dir.mkdir(BOX_OUTPUT_DIR)
    end

    # packerコマンドを生成して実行する
    command = "packer #{sub_command} -var 'box_output_dir=#{BOX_OUTPUT_DIR}' "
    VARIABLES.each do |var|
      if ENV.has_key?(var.upcase)
        command << "-var '#{var}=#{ENV[var.upcase]}' "
      end
    end
    command << "--only=#{builder_type} #{TEMPLATE_JSON}"
    sh command

    # debug
    command
  end

  # 生成されたboxファイルをS3にアップロードします
  #
  # Returns none
  def upload_box_to_s3
    # boxファイルは1件しかない想定。念のため1件以外の時は例外飛ばしておく。
    box_file_path =
      if Dir.glob("#{BOX_OUTPUT_DIR}/*").size == 1
        Dir.glob("#{BOX_OUTPUT_DIR}/*").first
      else
        raise "box files can only do not expect one"
      end

    # ビルドしたboxをS3に転送する
    Aws.config[:s3] = {
      region: ENV["REGION"],
      credentials: Aws::Credentials.new(ENV["AWS_ACCESS_KEY_ID"], ENV["AWS_SECRET_ACCESS_KEY"])
    }

    resp = Aws::S3::Client.new.put_object(
      bucket: ENV["S3_BUCKET"],
      body: File.open(box_file_path),
      key: "vagrant/virtualbox/#{File.basename(box_file_path)}",
      acl: "public-read"
    )

    puts "output vagrant box endpoint:"
    puts "  #{resp.context.http_request.endpoint.to_s}"

    # 転送したらboxファイルは不要なので削除
    File.unlink(box_file_path)
  end
end
```

### templateファイルにエラーがないかチェック
```
$ bundle exec rake packer:vb:validate
```

### ビルドを実行する
```
$ bundle exec rake packer:vb:build
```

私の環境(`MacBook Air (11-inch, Mid 2011), CPU: 1.8 GHz Intel Core i7, メモリ: 4 GB 1333 MHz DDR3`)では`1337.60 real 64.32 user 19.57 sys`で完了しました。

##### 備考
S3のアップロードにコケた場合、以下のコマンドでS3へのアップロードのみ再実行する

```bash
$ bundle exec rake packer:vb:s3upload
```

# リポジトリ
GitHub

https://github.com/fukuiretu/fukuiretu-sandbox/tree/master/packer_vb



# おわりに
これでVagrant up時の時間を短縮することができて、ストレスも軽減された