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

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

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

packer build base.json

f:id:kotatsu360:20160708125902p:plain

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のトークンに必要な権限については、公式ドキュメントで丁寧に紹介されていますので上では説明を省略しています。

Amazon AMI Builder - Packer by HashiCorp

Google Compute Builder - Packer by HashiCorp

ビルド対象について

packer build base.json

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

packer build -only='amazon-ebs' base.json

この場合、AMIだけビルドが始まります。

Build - Command-Line - Packer by HashiCorp

AWSのインスタンスタイプについて

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

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

Bug #1573231 “Kernel Panic on EC2 After Upgrading from 14.04 to ...” : Bugs : linux package : Ubuntu

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

現状の制約

この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に関してはスポットインスタンスを使うよう修正をしているところです。 ハイパフォーマンスのインスタンスが手頃な値段で使えるため、より快適なイメージ更新作業ができると思っています。

まとめ

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

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

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

最後に

VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。

また、VASILYでは今年もエンジニア向けインターンシップを行います。バックエンドチーム(インフラはバックエンドチームに所属しています)でのインターンシップについては、以下のリンクで募集しています。ご興味のあるかたは是非ご応募下さい。