Hashicorp Ottoを読む
Hashicorpから2015年秋の新作が2つ登場した.
Ottoがなかなか面白そうなのでコードを追いつつ,Ottoとは何か? なぜ必要になったのか? どのように動作するのか? を簡単にまとめてみる.
バージョンは 0.1.0 を対象にしている(イニシャルインプレッションである)
Ottoとは何か?
公式はVagrantの後継と表現されている.が,それはローカル開発環境の構築も担っているという意味で後継であり,自分なりの言葉で表現してみると「OttoはHashicorpの各ツールを抽象化し開発環境の構築からインフラの整備,デプロイまでを一手に担うツール」である.ちなみにOttoという名前の由来はAutomationと語感が似ているからかつ元々そういう名前のbotがいたからとのこと.
なぜOttoか?
なぜVagrantでは不十分であったのか? なぜOttoが必要だったのか? 理由をまとめると以下の5つである.
- 設定ファイルは似通ったものになる
- 設定ファイルは化石化する
- ローカル開発環境と同じものをデプロイしたい
- microservicesしたい
- パフォーマンスを改善したい
まず各言語/フレームワークのVagrantfileは似通ったものになる.Vagrantfileは毎回似たようなものを書く,もしくはコピペしていると思う.それならツール側が最も適したものを生成したほうがよい.Ottoは各言語のベストプラクティスな設定ファイルを持っておりそれを生成する.
そしてVagrantfileは時代とともに古くなる,つまり化石化する.秘伝のソースとして残る.Ottoは生成する設定ファイルを常に最新のものに保つ.つまり今Ottoが生成する設定ファイルは5年後に生成される設定ファイルとは異なるものになる(cf. “Otto: a modern developer’s new best friend”)
そしてローカル開発環境と同じものを本番に構築したい(Environmental parityを担保したい).現在のVagrantでもproviderの仕組みを使えばIaaSサービスに環境を構築することはできる.が本番に適した形でそれを構築できるとは言い難い.Ottoは開発環境の構築だけではなく,デプロイ環境の構築も担う.
時代はmicroservicesである.Vagrantは単一アプリ/サービスの構築には強いが複数には弱い.Ottoは依存サービスを記述する仕組みをもつ(Appfile).それによりmicroserviceな環境を簡単に構築することができる.
そしてパフォーマンス.最近のVagrantはどんどん遅くなっている.例えば立ち上げているVMの状態を確認するだけのstatusコマンドは2秒もかかる.Ottoはパフォーマンスの改善も目的にしている.
Ottoは何をするのか?
Ottoが行うことは以下の2つに集約できる.
- Hashicorpツールの設定ファイルとスクリプトを生成する
- Hashicorpツールのインストール/実行をする
Ottoの各コマンドと合わせてみてみると以下のようになる.
compile- アプリケーションのコンテキスト(e.g., 言語やフレームワーク)の判定と専用の設定ファイルであるAppfileをもとにHashicorpツールの設定ファイル(VagrantfileやTerraformの.tfファイル,Packerのマシンテンプレート.json)と各種インストールのためのシェルスクリプトを生成するdev- 開発環境を構築する.Vagrantを実行するinfra- アプリをデプロイするためのインフラを整備する.例えばAWSならVPCやサブネット,ゲートウェイなどを設定する.Terraformを実行するbuild- アプリをデプロイ可能なイメージに固める.例えばAMIやDocker Imageなど.Packerを実行するdeploy- 作成したイメージを事前に構築したインフラにデプロイする.Terraformを実行する(OttoのデプロイはImmutable Infrastructureを嗜好する)
Ottoがつくるインフラの基礎
OttoにはFoundationという概念がある(foundationという言葉は生成される設定ファイルやディレクトリ名に登場する).これはOttoが構築するインフラの基礎,本番環境にアプリケーションをデプロイするために重要となるレイヤーを示す.このFoundationの例としては,以下のようなものが挙げられる.
このレイヤーはモダンなアーキテクチャーではBest Practiceとされつつも構築はなかなか難しい.OttoはVagrantでローカル開発環境を構築するとき,本番環境のインフラを整備するときにこのレイヤーの整備も一緒に行う.
Ottoの設定ファイル
単純なことをするならばOttoには設定ファイルは必要ない.プロジェクトのルートディレクトリでcompileを実行すれば言語/フレームワークを判定し,それにあったVagrantfileとインフラを整備するためのTerraformの.tfファイルなどを生成してくれる.
より複雑なことをしたければ不十分である.Ottoは専用のAppfileという設定ファイルでカスタマイズを行うことができる.AppfileはHCLで記述する.例えば,以下のように依存するサービスを記述することができる
application {
dependency {
source = "github.com/tcnksm-sample/golang-web"
}
}
他にも,言語のバージョンを指定したり,デプロイするIaaSサービスやそのflavar(e.g., AWSだと現在simpleとvpc-public-privateがある.simpleは最小限のリソースを使うのみでScalabilityや耐障害性などは犠牲にする.vpc-public-privateだとprivateネットワークやNATなども準備する)を設定することができる.
基本は適切なデフォルト値と自動で判別される値が存在する.Appfileはそれを上書きするものである.公式の説明の仕方を借りるとAppfileは「どのようにマシンを設定するのかを記述するのではなく,アプリケーションが何であるかを記述する」ものである.
Ottoを読む
自分が気になった部分のソースコードを軽く読んでみる.
概要
上述したように,Ottoは各Hashicorpツールのバイナリを実行しているだけある.大まかには以下のようになる.
compile- 依存サービスがある場合はそれらを全て
.otto以下のディレクトリにfetchする(依存先もAppfileと.ottoidを持っている必要がある) - 各
Appfileと言語/フレームワークを判別結果をマージして.ottoディレクトリ以下に各種設定ファイルを生成する
- 依存サービスがある場合はそれらを全て
- コマンドごとに
otto/compiled以下の決められたディレクトリ内の設定ファイルをもとにバイナリを実行する- e.g.,
buildを実行するとPackerのマシンテンプレートである.otto/compiled/app/build/template.jsonが使われる
- e.g.,
コア
Ottoのコアはhttps://github.com/hashicorp/otto/blob/v0.1.1/otto/core.goにある.基本的にどのコマンドもここに到達する.やっていることは単純でコンテキストをもとに実行するべき設定ファイルを決めてそれを元にバイナリを実行するだけ.
以下をみると各バイナリをどのように実行しているかをみることができる.
- https://github.com/hashicorp/otto/tree/v0.1.1/helper/vagrant
- https://github.com/hashicorp/otto/tree/v0.1.1/helper/terraform
- https://github.com/hashicorp/otto/tree/v0.1.1/helper/packer
インストーラー
バイナリがインストールされていなければコマンド実行直後にインストールが実行される.https://github.com/hashicorp/otto/tree/v0.1.1/helper/hashitoolsにインストーラーが書かれている.以下のbintray.comのURLからzipをダウンロードして展開しているだけ.
url := fmt.Sprintf(
"https://dl.bintray.com/mitchellh/%s/%s_%s_%s_%s.zip",
i.Name, i.Name, vsn, runtime.GOOS, runtime.GOARCH)
(なんでmitchellhアカウントなのだろう…)
言語/フレームワークの判定
まずcompileのときにアプリケーションの言語/フレームワークの判定する方法.これはHerokuのBuildpackに似たことをする.アプリケーションに特有なファイル,例えばRubyならばGemfile,が存在するかをチェックする.判定のルールは以下のようなstructで保持する.
detectors := []*detect.Detector{
&detect.Detector{
Type: "go",
File: []string{"*.go"},
},
....
そして以下で判別する.単純.
func (d *Detector) Detect(dir string) (bool, error) {
for _, pattern := range d.File {
matches, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil {
return false, err
}
if len(matches) > 0 {
return true, nil
}
}
}
.hclファイルでDetectorを書いて~/.otto.d/detect以下に置い読み込むというロジックを見かけたので自分で好きな判定ロジックを定義できるかもしれない.
設定ファイル/インストールスクリプトはどこにあるのか?
“ハシコープは人類を含む全ての概念をバイナリにして配布した”
https://github.com/hashicorp/otto/tree/v0.1.1/builtin/app以下に各言語のVagrantfileやpackerのマシンテンプレート,それらが呼び出すインストールスクリプトが存在する.そしてOttoはそれらをgo-bindataを使ってバイナリとして埋め込んでいる.app.goの先頭をみるとそのためのgo generate文が見える.
//go:generate go-bindata -pkg=goapp -nomemcopy -nometadata ./data/...
時代はシェルスクリプト
Vagrantfileのインストールスクリプト,Packerのマシンテンプレートが呼び出すprovisionerのスクリプト,など全てがゴリゴリのシェルスクリプトで書かれている.Dockerfile以後,時代はシェルスクリプトになっている気がする.大変そう.
ちなみにデーモンの管理はupstartが使われている(cf. https://github.com/hashicorp/otto/blob/v0.1.1/builtin/foundation/consul/data/common/app-build/upstart.conf.tpl).
まとめ
とりあえず何か始めたいと思うときは便利であるし,期待感はある.が,最後にこれはどうなるんだろうと思ったことをまとめておく.
設定ファイルをバイナリに含めたら変更が辛くなるのではないか? もし設定ファイルに不備があったらそれを修正して新しくバイナリをリリースしないといけなくなる.利用者は開発者なので問題はなさそうだけど,一度ダウンロードしたものをすぐにアップグレードしてくれるだろうか(一応利用しているバイナリが最新であるかそうでないかを判定し,古い場合には警告を出す仕組みはある).Atlasを使ってファイルをホストする方式ではだめだったのか? boxを使うのはダメだったのか?(重いかな..).
今のところcompileするたびに.ottoディレクトリは作り直される.同じ環境であることは担保するのは.ottoidファイルしかない.これはどこまで非互換な変更を対処してくれるのか.ローカル開発環境は良いが,デプロイがぶっ壊れることはないだろうか.. (が,これはottoというよりはTerraformの問題な気もする).