Vagrantfile をコミットするために

趣味のプロジェクトで Vagrant を利用していると、作ることを優先して細かいことに気を使わないので、公開リポジトリには置きにくい設定がいくつも入り込んでしまう。なので、今までの私は、GitHub にコードはコミットするけど Vagrantfile はコミットせず、別の所で作って管理していた。でも、１つのプロジェクトで複数のリポジトリを管理するのは面倒だし、他の人と一緒に開発を始めたときの共有方法に困った。

ここでは、この残念な状態から脱出するために、コミットをためらう要素とこれらを排除するために私が取った方法について説明したい。
きっともっとマシな方法があると思う。もっとスマートな方法を知っていたら教えて欲しい。

# コミットをためらう要素

私の場合は以下のような問題で、コミットをためらっていた。

* vagrant の外側で追加の作業が必要
* node_modules のシンボリックの作成
* シークレットキー等の開発環境についての設定を含めていた
* 個人的な設定を取り込むコードを直接書いていた

## vagrant の外側で追加の作業が必要

せっかくVagrant を使っているのだから理想的には以下のようなステップで開発環境は構築したい。

```bash
> git clone git@github.com:xxxx/xxxx.git
> vagrant up
> vagrant ssh
```

でも、私の作った Vagrantfile では以下のような追加の作業を必要としていた。

* node_modules のシンボリックリンクの作成
* シークレットキー等の設定

これらの作業が原因で、新しく開発環境を構築するときには、上の３行以上の作業が必要だった。しかも、間違えやすい。

自分のためにも他人のためにも、事前の訳の分からない作業は極力なくしたい。

## node_modules のシンボリックの作成

Windows 上で Vagrant を使って nodejs を使ったアプリを開発しようとすると、node_modules に関連するトラブルに遭遇した。

最初はパス長が長すぎてファイルが作れないという問題だった。この対策として、Windows に関係のない仮想マシン上のディスクに tmp_node_modules のようなディレクトリを作成して、プロジェクトのディレクトリからシンボリックリンクを張るという解決策をとった。

これで、問題は解決し、私はアプリを作り始めることが出来た。だが、このために、管理者権限のコマンドプロンプトを２回ほど使う必要がある。シンボリックリンクを使う許可とシンボリックリンクを作る操作が必要だった。詳細は理解していない。このために管理者権限のコマンドプロンプトを使わなくてはならない。新しい PC で開発を始めるたびにこの作業が必要になる。私の場合は、デスクトップPC と ノートPCで数回この設定を繰り返した。

事前にこんな手間が必要な Vagrantfile は使いたくない。私は開発環境を構築し開発を始めたときから、Vagrantfile をコミットしたくないと感じてしまった。

## シークレットキーの設定

最近、OAuth や OpenID Connect 等の Google や GitHub のアカウントを使ったログインをサポートしているアプリが多いので、私もこれに乗りたいと思った。これらの API を使う場合には、事前の申請が必要で、申請すると、クライアントキーや、シークレットキーというものが発行されるようだ。これを自分のプログラムに組み込まないといけない。また、これらの値は基本的には外部に漏らして良いものではないらしい。

最初は、コードに埋め込んでいたが、流石にこのままだと全くコミットできないので、実験が終わったタイミングで、環境変数に追い出すことにした。

ここまでは良かったが、次は、Vagrant が作った仮想マシン内でどうやって環境変数を取り込むのか、という問題に当たった。

最近までは、専用にシェルスクリプトを書いて、仮想マシンに入るたびに実行される場所に設定していた。当然、これらのスクリプトは私のホームディレクトリの構造に依存していて、コミットするのはためらわれる状態だった。

このシークレットキーの問題については、無くすことができない必要な作業だと思う。問題は、他の人が簡潔に自分のキーを使えるようにする方法が無いということだ。流石に、 $HOME/Dropbox/project_name/setup-secretkey.sh に設定があることを前提とするのは間違っていると思う。

## 個人的な設定を取り込むコード

仮想マシン内でも zsh と自分の zshrc を使いたい。このために、 Vagrantfile 内で、 synced_folder で自分の dotfiles のディレクトリを同期して、プロビジョニングで、セットアップスクリプトを走らせていた。

ここまでは、そんなに悪い話だとは感じないが、私は、これらの設定を Dropbox で複数のPC間で共有している。なので、 Vagrantfile には私の個人的な設定に依存した Dropbox のパスがそのまま書かれていた。他の人が必要とするとは限らないし、同じパスを使っている可能性はほとんど無い。そんな設定が必須な Vagrantfile のコミットは避けるしかなかった。

# コミットするための条件

多くのコミットしたくない要因を Vagrantfile に差し込んでしまった私は、Vagrantfile をリポジトリにコミットするためには以下の条件は満たすべきだと考えるようになった。

* "vagrant up" のみで誰でも開発環境を構築できる
* 外部から注入するべきパラメータが自明である
* 個人的な設定を取り込む余地がある
* 同種の他のプロジェクトでもすぐに流用できる

## "vagrant up" のみで誰でも開発環境を構築できる

このために、Vagrant は作られている。これ以外の事前準備はなくすべきだ。

これを達成するために試行錯誤した結果は以下で説明していく。

## 外部から注入するべきパラメータが自明である

シークレットキーのようなサービスを立ち上げるのに固有で必要な値は、外から注入するしか無い。これについては、何をすべきなのか、自明で一般的な方法が使えるようにしたい。

具体的には環境変数で設定できるようにするべきだと考えた。

## 個人的な設定を取り込む余地がある

みんな同じ環境を構築できることは理想だが、全く同じ環境しか使えないというのは嫌だと思う。自分が長い間使っているショートカットやエイリアスは持ち込みたい。

ここで重要なのは、個々人で固有のセットアップを仮想マシンの中に持ち込む方法だ。ここでも環境変数を使うのが良いと思う。当然ながらこの設定がなくても問題なく開発環境を構築できないといけない。

## 同種の他のプロジェクトでもすぐに流用できる

１つの開発環境の構築が簡単なだけではなく、他のプロジェクト向けにも簡単に流用したい。

ここまでに説明してこなかった話題になるが、今まで私が作ってきた Vagrantfile 内には自分が作っているプログラムの名前が含まれていた。それでも、新しいプロジェクトを始めるときには、最後に触っていた Vagrantfile をコピペして持ってくるので、最初の作業として、 Vagrantfile の中のプロジェクト名やアプリ名の置換が必要だった。そして、よくミスをして、最初の環境構築に手間取っていた。

完全に避けるのは無理だとは思うが、依存は減らしておきたい。

# 解決手段

週末を２日分ほど使って調べた結果、今は以下のような解決手段に落ち着いている。

## bindfs や "mount --bind" を使う

node_modules ディレクトリにシンボリックリンクを張るのは Windows 上では負担が大きいので、他の手段で node_modules を他の場所に追い出したかった。

私が探した中では、 bindfs が最も良い解決手段だった。

http://bindfs.org/

bindfs は OS にある FUSE (Filesystem in Userspace) の機能を使って、ディレクトリをディレクトリにマウントすることができる。リンク先にもあるが "mount --bind" でも同じことができる。bindfs はパーミッションを追加で操作できるので、強い権限で共有されているファイル群を弱めの権限でマウントすることができる。

具体的には、以下のようなスクリプトで仮想マシン上のディレクトリを node_modules にマウントしている。

```rb
config.vm.provision "bind directories",
    type: "shell",
    run: "always",
    privileged: false,
    inline: <<-SHELL
        mkdir -p ~/tmp_node_modules
        mkdir -p ~/app/node_modules        
        
        # mount --bind の場合
        sudo mount --bind /home/vagrant/tmp_node_modules /home/vagrant/app/node_modules
        
        # bindfs の場合
        sudo bindfs /home/vagrant/tmp_node_modules /home/vagrant/app/node_modules -p 0755 -u vagrant
    SHELL
```

これで Windows 上でシンボリックリンクを張る面倒な作業がなくなって、Vagrantfile 内での完結した操作で済むようになった。

### その他の候補

他の手段としては以下のようなものを調べたり実験したりしていたが、どれも良い解決にはならなかった。

* rsync を使う
一方向の同期のみなので問題が出なさそうだったが、仮想マシンからも編集したいので目的に合わなかった。

* node_modules がリポジトリの外に置かれるようにディレクトリ構成を改める
考えてみただけだが、一般的なディレクトリ構成から外れた道なので検討するだけで諦めた。

* nfs を使う
もしかしたらシンボリックリンクを雑に扱ってくれるかもしれないと考えたが、普通に Windows 上でシンボリックリンクを作るときと同じようなエラーがでて終わった。

* 管理者権限でセットアップを実行するスクリプトを作る
runas コマンドを使えばできそうな気配がするが、このためだけに専用のスクリプトを作りたくなかったし、 Vagrant や VirtualBox が実行中の場合はうまく動かなかったりして問題が多そうだったので試すこともなかった。

## 環境変数から設定を取り込むようにする

理想的には外の環境で設定されている環境変数のうち、選択した変数だけが仮想マシンの中にも定義されて欲しい。

Vagrantfile 内は `ENV['CLIENT_ID']` のような形で環境変数にアクセスしているということはすぐに分かった。ヒアドキュメントによるプロビジョニングのスクリプト内でも `#{ENV['CLIENT_ID']}` という表記でアクセスできることが分かった。

でも、`vagrant ssh` で入った先の環境で環境変数を定義する方法が分からなかった。これについては、以下で説明するように `/etc/profile.d` に設定用のスクリプトを書き込むことで解決した。

## /etc/profile.d に設定用スクリプトを置く

調べていたところ、 `/etc/profile.d/` においたスクリプトはすべてログイン時に実行されることがわかったので、使えそうだと分かった。このディレクトリの目的やマナー的にどうなのかは知らない。

以下のようなスクリプトでシークレットキーを設定することにした。

```rb
config.vm.provision "export env",
    type: "shell",
    run: "always",
    privileged: true,
    inline: <<-SHELL
        echo "# vagrant script for every boot" > /etc/profile.d/vagrant.sh
        echo export CLIENT_SECRET=#{ENV['CLIENT_SECRET']} >> /etc/profile.d/vagrant.sh 
        echo export CLIENT_ID=#{ENV['CLIENT_ID']} >> /etc/profile.d/vagrant.sh            
        chmod +x /etc/profile.d/vagrant.sh
    SHELL
```

## dotfiles のセットアップを環境変数でコントロールする

仮想マシン内に個人的な設定を持ってくるためには以下の処理が必要になる。

* 設定と設定を反映するためのスクリプト群を仮想マシン内に共有する
* 設定を反映するためのスクリプトを実行する

すべての人が個別の設定を必要とするわけではないので、使わないことも選べる必要がある。

これらを考えて以下の環境変数を定義して使うことにした。

* USE_DOTFILES
1 と定義されている場合のみ個別の設定を取り込み使うようにすることにした。

* DOTFILES_DIR
共有するディレクトリを指定する。これで Dropbox/settings とか個人的なパスを登場させずに済む。

* DOTFILES_SETUPSCRIPT_NAME
共有するディレクトリ内でのセットアップ用のスクリプトの名前を指定する。

結果としては以下のような設定を Vagrantfile に書くようにした。

```rb
if ENV['USE_DOTFILES'] == "1" then
    config.vm.synced_folder ENV['DOTFILES_DIR'], "/home/vagrant/dotfiles"
    config.vm.provision "setup dotfiles",
        type: "shell",
        privileged: false,
        inline: <<-SHELL
            script=/home/vagrant/dotfiles/#{ENV['DOTFILES_SETUPSCRIPT_NAME']}
            echo dotfiles source dir: #{ENV['DOTFILES_DIR']}
            if [ -f $script ]; then
                echo setup dotfiles by $script
                $script
            else
                echo $script is not found
            fi
        SHELL
```

## ディレクトリ構成を汎用的なものにする

複数のプロジェクトで使いまわせるようにディレクトリ構成は以下のようにすることとした。

```
/home/vagrant
/home/vagrant/app
/home/vagrant/dotfiles
/home/vagrant/tmp_node_modules
```

vagrant ユーザのホームディレクトリ以下には app と dotfiles と tmp_node_modules を配置する。

app 以下はリポジトリで管理されたソースコードが配置される。 app 以下にはVagrantfile も置かれている。dotfiles は .zshrc などと一緒にそのセットアップスクリプトも置かれている。 tmp_node_modules は app/node_modules にマウントされる。

# 結果

長い試行錯誤の結果、以下のような Vagrantfile が出来上がった。

```rb
Vagrant::configure("2") do |config|
    config.vm.provider "virtualbox" do |v|
        v.linked_clone = true
        v.memory = 1024
    end

    config.vm.hostname = "vagrant"
    config.vm.box = "ubuntu/trusty64"
    config.vm.synced_folder ".", "/home/vagrant/app"

    # default port
    config.vm.network "forwarded_port", guest: 3000, host: 3000
    # debug port
    config.vm.network "forwarded_port", guest: 9229, host: 9229

    config.vm.provision "install tools",
        type: "shell",
        privileged: false,
        inline: <<-SHELL
            export DEBIAN_FRONTEND=noninteractive
            sudo apt-get install -y wget curl 
            sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927
            echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list
            curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
            
            sudo apt-get update
            sudo apt-get install -y git zsh whois nodejs emacs lv bindfs
            sudo apt-get install -y mongodb-org=3.2.9 mongodb-org-server=3.2.9 mongodb-org-shell=3.2.9 mongodb-org-mongos=3.2.9 mongodb-org-tools=3.2.9
            wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh
        SHELL

    config.vm.provision "export env",
        type: "shell",
        run: "always",
        privileged: true,
        inline: <<-SHELL
            echo "# vagrant script for every boot" > /etc/profile.d/vagrant.sh
            echo export CLIENT_SECRET=#{ENV['CLIENT_SECRET']} >> /etc/profile.d/vagrant.sh 
            echo export CLIENT_ID=#{ENV['CLIENT_ID']} >> /etc/profile.d/vagrant.sh            
            chmod +x /etc/profile.d/vagrant.sh
        SHELL

    config.vm.provision "bind directories",
        type: "shell",
        run: "always",
        privileged: false,
        inline: <<-SHELL
            mkdir -p ~/tmp_node_modules
            mkdir -p ~/app/node_modules        
            sudo bindfs /home/vagrant/app /home/vagrant/app -p 0755 -u vagrant
            sudo bindfs /home/vagrant/tmp_node_modules /home/vagrant/app/node_modules -p 0755 -u vagrant
        SHELL
    
    if ENV['USE_DOTFILES'] == "1" then
        config.vm.synced_folder ENV['DOTFILES_DIR'], "/home/vagrant/dotfiles"
        config.vm.provision "setup dotfiles",
            type: "shell",
            privileged: false,
            inline: <<-SHELL
                script=/home/vagrant/dotfiles/#{ENV['DOTFILES_SETUPSCRIPT_NAME']}
                echo dotfiles source dir: #{ENV['DOTFILES_DIR']}
                if [ -f $script ]; then
                    echo setup dotfiles by $script
                    $script
                else
                    echo $script is not found
                fi
            SHELL
    end
end
```