Serverkitつくった

4/6 API納品

ChefやAnsible、Puppet、Itamaeなどの構成管理ツールをあまり使ったことがなく、勉強のためにServerkitというのをつくってみたので、現状こういう感じでやってみましたというのを書き残しておく。作り手の気持ちになればこそわかるものがあるだろうと思う。 ところで去年も似たような記事を書いた。

概要

Serverkitというのは、前述した通りChefやAnsibleのような構成管理ツール。マシンの理想的な状態をレシピと呼ばれるファイルに定義しておき、現在のマシンの状態と比較してその差分を埋めるためのもの。Rubyで書かれていて、手元にversion 2.0.0以上のRubyと、Serverkit、それからServerkitが利用している幾つかのライブラリが入っていれば動作する。Serverkitを動かすマシンと同じマシン、もしくはSSHで接続できるマシンに対して実行できる。このとき、SSHで接続する先のマシンには特に何もインストールされていなくてもよい。

使い方

serverkit checkコマンドを使うと、レシピに書かれた理想の状態とマシンの状態を比較して、変更が必要そうな箇所を一覧できる。例えば対象がMacbookで、homebrewでMySQLが入っていてほしい旨のレシピが書かれていた場合、MySQLがインストールされているかどうかを調べることになる。他の構成管理ツールのdry-runに似ているかもしれない。serverkit applyコマンドを使うと、実際にその変更を埋めるための処理を適用できる。先述した例であれば、もしMySQLがhomebrewでインストールされていなければ homebrew install mysql が実行されることになる。

$ serverkit check recipe.yml
$ serverkit apply recipe.yml

レシピ

レシピはYAMLJSON、あるいはJSONを出力する実行可能ファイルや、それらを出力するERBテンプレートとして記述できる。例えば単純な形式で書くと以下のような感じになる。resourcesプロパティの配列の要素をresourceと呼ぶが、各resourceにはtypeプロパティが必須で、例えばtypeがhomebrewであれば、nameで指定された名前のパッケージがインストールされているという状態を表現できる。また、typeがgitであれば、repositoryで指定されたURLのGitレポジトリが、pathで指定されたディレクトリにgit-cloneされているという状態を表現できる。

{
  "resources": [
    {
      "id": "install_mysql",
      "type": "homebrew",
      "name": "mysql"
    },
    {
      "id": "clone_dotfiles",
      "type": "git",
      "repository": "git@github.com:r7kamura/dotfiles.git",
      "path": "/Users/r7kamura/src/github.com/r7kamura/dotfiles"
    }
  ]
}

レシピの指定

実行時には、コマンドの引数でレシピのパスを与える。また、レシピを記述したファイルパスの代わりにディレクトリのパスを指定することもでき、この場合ディレクトリ内部のレシピが再帰的に読み込まれることになる。例えばカレントディレクトリから見て以下のようにファイルが配置されていた場合、上から順にレシピが読み込まれ、全てのresourcesが1つの配列にまとめられた状態で実行されることになる。

$ tree recipes -L 2 --charset unicode
recipes
|--a.yml
|--b.json
|--c.json.erb
`--d
   |--e.yml
   `--f.json
$ serverkit apply recipes

レシピの表示

ERBテンプレートや、実行可能ファイル、DSL、ディレクトリパスでの指定などを利用していると、最終的にどのような処理が実行されるのか、dry-runに相当するコマンドを利用してみるまで分からない、ということになりがち。そういう場合のために、それらのレシピを展開して最終的に1つのレシピにまとめたものをJSON形式で出力する serverkit inspect というコマンドを用意してみた。大規模になってきたら便利かもしれない。

$ serverkit inspect recipes
{
  "resources": [
    {
      "id": "install_mysql",
      "type": "homebrew",
      "name": "mysql"
    },
    {
      "id": "install_redis",
      "type": "homebrew",
      "name": "redis"
    },
    {
      "id": "install_licecap",
      "type": "homebrew_cask",
      "name": "licecap"
    },
    {
      "id": "install_alfred",
      "type": "homebrew_cask",
      "name": "alfred"
    },
    {
      "id": "clone_dotfiles",
      "type": "git",
      "repository": "git@github.com:r7kamura/dotfiles.git",
      "path": "/Users/r7kamura/src/github.com/r7kamura/dotfiles"
    },
    {
      "id": "symlink_zshrc",
      "type": "symlink",
      "source": "/Users/r7kamura/.zshrc",
      "destination": "/Users/r7kamura/src/github.com/r7kamura/dotfiles/linked/.zshrc"
    }
  ]
}

レシピの検閲

レシピの内容に誤りがないか実行前に調べるために、serverkit validate というコマンドも用意してみた。例えば、レシピはHash型でArray型のresourcesプロパティを持ち、resourcesの各要素はHash、それぞれのresourceはtypeプロパティを持ち、typeがfileであればsourceとdestinationプロパティが必須、またsourceで指定されたパスには実際にファイルが存在している必要がある、といったルールに従い、違えていればエラーを出力してくれる。余談になるが、validationの定義にはRailsなどで使われているActiveModel::Validationsを利用した。attribute :source, required: true, readable: true のようにValidation定義が書けて、適切なエラーメッセージも用意されて便利。

$ serverkit validate recipe.yml
Error: source attribute is required in file resource
Error: path attribute can't be unreadable path in recipe resource

ERBテンプレートと変数

動的にレシピを組み立てるというのは実行可能ファイルをレシピに利用すれば実現できるが、同じユーザ名を参照する部分があるので変数を利用してまとめたいとか、別の名前のパッケージを複数インストールするために似たような繰り返し処理をまとめたい、などの理由だけでわざわざ実行可能ファイルを書くには大袈裟すぎる。その辺りの用途のために、ファイル名の末尾に .erb を付けるとERBテンプレートとして先に展開した後、そのファイルを処理するようにした。serverkitでは末尾の拡張子部分でファイルの処理方法を判断する性質があるので、ファイル名が foo.json.erbであればERBテンプレートをfoo.jsonに展開して、次にJSONとして処理するという感じになる。

またERBテンプレートのために、レシピの外部から変数を与える方法も用意してみた。--variables= というコマンドラインオプションでパスを指定できて、レシピと同じようにYAMLJSON、実行ファイル、ERBテンプレート、ディレクトリなどが指定できるようになっている。使うときは以下のような感じで使える。ERBの中では、variables.yml で与えたHashのキーと同名のメソッドを参照できる。余談だが、こういう風に変数をメソッドとして参照できるようにするために Hashie::Mash を使ったりした。

$ serverkit apply recipe.yml.erb --variables=variables.yml
# variables.yml
dotfiles_repository: r7kamura/dotfiles
user: r7kamura
# recipe.yml.erb
resources:
  - id: clone_dotfiles
    type: git
    repository: git@github.com:<%= dotfiles_repository %>.git
    path: /Users/<%= user %>/src/github.com/<%= dotfiles_repository %>
  - id: symlink_zshrc
    type: symlink
    source: /Users/<%= user %>/.zshrc
    destination: /Users/<%= user %>/src/github.com/<%= dotfiles_repository %>/.zshrc

リソース

レシピのresourcesプロパティの要素として与えるそれぞれのHashのことをresourceと呼んでいるが、この辺は後から増やせると思ってサボっていたせいで、現状種類が非常に少なく、resourceごとの細かい状態もまだほとんど指定できない (例えばファイルのownerを指定できないとか)。この辺は後々使いたいものから順に追加していきたいし、追加されたい。

  • file
  • git
  • homebrew
  • homebrew_cask
  • package
  • recipe
  • service
  • symlink

ロール

過度に複雑性が増すことを恐れて、ChefのRoleやNodeのような高機能なものは用意しなかった。ただ、複数のresourcesの定義を機能ごとにまとめたいとか (例えばnginx関係をまとめるとか)、外部から与える変数によって利用するレシピを変えたいとか (例えば検証用環境と本番環境で少し変えるとか)、そういう要件があるのは理解できるので、代わりに「他のレシピをincludeできるresource」というのを用意してみた。これで特定の関心事の単位でresourcesをある程度まとめられて、variablesとERBテンプレートを利用して利用するレシピを変えるというのもできるようになる。

# recipe.yml
resources:
  - id: install_homebrew_packages
    type: recipe
    path: homebrew.yml
  - id: install_homebrew_cask_packages
    type: recipe
    path: homebrew_cask.yml
  - id: install_dotfiles
    type: recipe
    path: dotfiles.yml.erb

おわり

とりあえず1週間ほど前から深夜アニメを見ながら気長につくってみており、現状こういう感じでやってみたという内容をまとめました。使っていくうちに要件が見えていくだろうと思うので、まずは自宅用のMacbookと職場用のMacbookの構成管理ツールとして使いながら、リソースの種類を増やしていけると良さそう。適当なアプリケーションサーバに適用できる段階になってくれば、AnsibleのvaultやChefのencrypted data bagのような機能なども付けていくかもしれないし、単機能用途と割りきって付けていかないかもしれない。

機能を実装するというのは、極論で言えばまあある程度時間を掛ければできていくけれど、使ってもらえるようなものにするには、そのための要件を拾い集めたり、途中で詰まないように逃げ道を残して安全に仕様を固めていったり、ちゃんと使えるものであるという信頼を得たり、使われるためにコミュニティに参画、また運営し、困っている人がいたら助けてあげる、また自分だけでなく周りの人たちも巻き込んでいく、そういう空気をつくっていく、というところへの絶え間ない努力がまた必要なわけで、先を見ると道は長いし、上手くいくかどうかも怪しい。作り手の気持ちになればこそわかるものというのはこういうもので、また再確認できて嬉しいし、こういうことをまだ嬉しく感じられるというのがまた嬉しくもある。