PackerでESXi上に仮想マシンをワンコマンドで構築できるようにした話＋躓いたこと

# シチュエーション

ESXiは会社でよく使っている仮想化環境なのですが、操作をおこなうためのvSphere ClientにOSX用のものがなく、Macbookからは別の方法で操作する必要がありました。
調べているうちに行き着いたのが [SSHでログインして専用コマンドで操作する方法](http://qiita.com/binzume/items/096ce77f73b6e853c342) だったのですが、専用のコマンドをいくつかと、vmxファイルの設定項目について覚える必要があり、これは自分が覚えておくにも、他のメンバーに教えるにも若干しんどいなーと感じました。
それでうまいこと楽できないかと思って探していたら、Packerのvmware-iso builderのオプションで、ESXiにログインしてイメージを作るというものがあったので、これでやってみることにしました。
結果としてワンコマンドでOSのインストール完了までできるようにはなったのですが、そこまでにハマったことがあり、それに関する情報で日本語のものが見つからなかったものがあったので、ここに書き残します。
折よく12月であり、 [HashiCorp Advent Calendar 2015](http://qiita.com/binzume/items/096ce77f73b6e853c342) が開催中でしたので、その20日目に参加させてもらいます。

# 前提

* ESXiの設定でSSHログインが可能になっている必要があります。
* [公式](https://www.packer.io/docs/builders/vmware-iso.html) にも書かれている通り、GuestIPHackが有効になっている必要があります。
* 構築処理を実行するマシンから公開鍵をESXiサーバーに登録しています。公開鍵を置くべきディレクトリは `.ssh/authorized_keys` ではなく、sshdの設定に書かれているので、参照して従うなり変えるなりすると良いでしょう。
 * 関連した驚きとして、sshでESXiサーバーに入ると、ホームディレクトリが `/` なんですよね
* 下記実装中、 `YOUR_なんたら` ってしてるところは、あなたの状況に合わせた値に読み替えてください。

# 実装

Atlas Packer Vagrant Tutorial用の [公式レポジトリ](https://github.com/hashicorp/atlas-packer-vagrant-tutorial) をベースに、必要な部分に関して修正追加を行ったものです。

## テンプレートファイル

```json
{
    "variables": {
      "vm_name": "",
      "_comment": "usernameとpasswordを変更する場合はpreseed.cfgも変更が必要です",
      "vm_username": "YOUR_VM_USERNAME",
      "vm_password": "YOUR_VM_PASSWORD",
      "vmhost_hostname": "YOUR_VMHOST_HOSTNAME",
      "vmhost_username": "YOUR_VMHOST_USERNAME",
      "vmhost_password": "{{ env `VMHOST_PASSWORD` }}"
    },
    "provisioners": [
        {
            "type": "shell",
            "scripts": [
                "scripts/base.sh",
                "scripts/vmware.sh",
                "scripts/vagrant.sh",
                "scripts/dep.sh",
                "scripts/cleanup.sh",
                "scripts/zerodisk.sh"
            ],
            "override": {
                "vmware-iso": {
                    "execute_command": "echo '{{ user `vm_password` }}'|sudo -S bash '{{.Path}}'"
                }
            }
        }
    ],
    "builders": [
        {
            "type": "vmware-iso",
            "boot_command": [
                "<esc><esc><enter><wait>",
                "/install/vmlinuz noapic preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ",
                "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ",
                "hostname={{ .Name }} ",
                "fb=false debconf/frontend=noninteractive ",
                "keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA keyboard-configuration/variant=USA console-setup/ask_detect=false ",
                "initrd=/install/initrd.gz -- <enter>"
            ],
            "boot_wait": "10s",
            "disk_size": 102400,
            "guest_os_type": "Ubuntu-64",
            "headless": true,
            "http_directory": "http",
            "iso_checksum": "a3b345908a826e262f4ea1afeb357fd09ec0558cf34e6c9112cead4bb55ccdfb",
            "iso_checksum_type": "sha256",
            "iso_url": "http://releases.ubuntu.com/14.04/ubuntu-14.04.3-server-amd64.iso",
            "skip_compaction": true,
            "ssh_username": "{{ user `vm_username` }}",
            "ssh_password": "{{ user `vm_password` }}",
            "ssh_port": 22,
            "ssh_wait_timeout": "10000s",
            "shutdown_command": "echo '/sbin/halt -h -p' > shutdown.sh; echo '{{ user `vm_password` }}'|sudo -S bash 'shutdown.sh'",
            "tools_upload_flavor": "linux",
            "remote_type": "esx5",
            "remote_host": "{{ user `vmhost_hostname` }}",
            "remote_username": "{{ user `vmhost_username` }}",
            "remote_password": "{{ user `vmhost_password` }}",
            "vmx_data": {
              "ethernet0.virtualDev": "vmxnet3",
              "ethernet0.networkName": "VM Network",
              "ethernet0.present": "TRUE",
              "ethernet0.addressType": "static",
              "ethernet0.Address": "00:50:56:3f:ff:ff",
              "numvcpus": "2",
              "memSize": "2048"
            },
            "vm_name": "{{ user `vm_name` }}"
        }
    ]
}
```

## ラッパースクリプト

```bash
#!/bin/bash

if [ $# -ne 2 ]; then
  echo "usage: ./create_vm.sh vmhost_hostname vm_name"
  exit 1
fi

vmhost_hostname=$1
vm_name=$2
vmhost_username=YOUR_VMHOST_USERNAME
datastore_path=/vmfs/volumes/datastore1
output_path=${datastore_path}/output-vmware-iso
vm_directory_path=${datastore_path}/${vm_name}
vmx_path=${vm_directory_path}/${vm_name}.vmx
echo "Registering vm ${vm_name} to ${vmhost_hostname}"

packer build -var "vmhost_hostname=${vmhost_hostname}" -var "vm_name=${vm_name}" template.json && \
ssh "${vmhost_username}@${vmhost_hostname}" "mv ${output_path} ${vm_directory_path}" && \
ssh "${vmhost_username}@${vmhost_hostname}" "mv ${vmx_path} ${vmx_path}.orig" && \
ssh "${vmhost_username}@${vmhost_hostname}" "sed -e 's/ethernet0.addresstype = \"static\"/ethernet0.addresstype = \"generated\"/g' ${vmx_path}.orig | grep -v 'ethernet0.address = ' > ${vmx_path}" && \
vmid=$(ssh "${vmhost_username}@${vmhost_hostname}" "vim-cmd solo/registervm ${vmx_path} ${vm_name}") && \
ssh "${vmhost_username}@${vmhost_hostname}" "vim-cmd vmsvc/power.on ${vmid}"
```

## preseedファイル

```
## 変更点のみ抜粋

## これを変更してないと、インストール終了後にログインできなかった
d-i passwd/user-fullname string YOUR_VM_USERNAME
d-i passwd/username string YOUR_VM_USERNAME
d-i passwd/user-password password YOUR_VM_PASSWORD
d-i passwd/user-password-again password YOUR_VM_PASSWORD

## この指定がないとインストーラーがbootdevを聞くところで停止してた
d-i grub-installer/bootdev string /dev/sda
```

# 解説

## Packerの実行中に仮想マシンのIPアドレスが変わらないようにして、構築の中断を回避する

vmware-iso builderはOSのインストールディスクをダウンロードしてきてインストールを実行します。実行が終わるのを待ってprovisionerに処理を渡しますが、この待ち方が「インストール開始時のIPアドレスにSSH接続できるようになるまで待つ」というものです。
一方で、ESXi上の仮想マシンのNICの設定でMACアドレスの設定を自動にしていると、起動する度にMACアドレスが変わってしまうので、DHCPを利用していると、インストール後の再起動時にIPアドレスが割り振り直されてしまい、builderの待ち受けに応えることができず、ここで構築処理が止まってしまいます。

そこで、これを回避するために、「構築中はMACアドレスを固定し、構築終了後に自動割り振りに戻す」作戦を取ることにしました。固定するための設定はテンプレートに、自動割り振りに戻す処理（vmxファイルを直接書き換えてます）はラッパースクリプトに記述しています。

固定IPアドレスが最初から決まっている場合には引数で与えられるようにすることはできる気がします。また、環境が許すならば、構築処理用の固定IPアドレスを用意する手もありそうです。

## 構築が終わったあとESXiサーバーに仮想マシンを再登録する

vmware-iso builderは構築終了時にESXiサーバーから仮想マシンを登録解除してしまいます。
また、builderの設定でESXiサーバー上のdatastoreは指定できるのですが、その下の仮想マシンを置くディレクトリ名は固定なので、そのままでは次に別のマシンを構築するときに一部のファイル名が衝突して構築が行えません。

これを私はbuilderは「構築はするがその後の扱いについては関知しない」というポリシーを取っているのだと理解しました。なのでその後の扱いとして

* 適切なディレクトリに仮想マシン関連のファイルを移動させる
* 仮想マシンを再登録する（ついでにパワーオンもする）

処理をラッパースクリプトに書きました。いろいろ試しながら書いてこの書き方に落ち着きましたが、vSphere post-processorをうまいこと使えばもっと楽にできそうな気はしてます。

## マシンスペックを指定する

仮想ディスクの大きさだけはbuilder本体のdisk_size設定（単位はMB）に書くことになりますが、それ以外はvmx_data項目を設定してvmxの設定項目を上書きする形で指定することになります。今回の設定で言うと、numvcpusがCPUコア数、memSizeがメモリ容量（単位はMB）です。

# 最後に

構築処理を実行して、その様子をwindowsのvSphere Clientから見ていると、勝手にvmができて、そこにvncにログインして、ブートコマンドを叩いている様子を見ることができます。また、構築処理を実行するマシンが勝手に開いてるポートを探してhttp待受を開始して、preseedファイルを配信したりもします。そのパワフルさは自分にとっては圧巻に感じられ、良い時代になったものだと思いました。
今後も便利技術にどんどんのっかって行きたいです。