AWS
Vagrant
packer
gce

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

More than 1 year has passed since last update.


この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事もご覧ください。



Qiita追記

2016.07.21にUbuntu16.04.1が公開されました。各プラットフォームのベースイメージにも用意されています。

本文中で参照しているイメージのOSはUbuntu16.04と少し古いため、適宜IDを更新して下さい。

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


本文

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

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


packer build base.json

発表資料はこちら(speederdeck)

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

  "variables":{

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

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

packer/base.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


base.jsonの中身

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

$ 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にコミットするときも安全です。

  "variables":{

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


builders

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

  "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を一度VirtualBoxに入れて、OVF2.0でエクスポートしたものです。

Ubuntu14.04だとCanonical公式のboxをtarで解凍したときに出てくるovaをpackerで読み込めたのですが、Ubuntu16.04のboxから同様に作成したovaは読み込みエラーになったため自分で作成しました。


provisioners

  "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と衝突するのを避けるためです。

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

# 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

  "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


その他補足


トークンの権限について

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

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

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


ビルド対象について

packer build base.json

と実行すると3つのプラットフォームでビルドが始まりますが、対象を絞ることもできます。

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の書き方が悪いのか、ある時から消えなくなってしまいました。

# 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を実行して下さい。

packer build -force base.json


二つ目:バージョンの手動変更

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

  "variables":{

"version": "1.0.2",

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


今後やりたいこと

まだまだこなれておらず、日々jsonを更新しています。

例えば、EC2に関してはスポットインスタンスを使うよう修正をしているところです。

ハイパフォーマンスのインスタンスが手頃な値段で使えるため、より快適なイメージ更新作業ができると思っています。


まとめ

かなり駆け足ではありましたが、実際に運用しているpacker用のJSONファイルについてご紹介いたしました。

packerは簡単に始められますが、ちょっと凝ったことをしようと思うとやはりオプションの調整が避けられません。

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

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