クックパッドのサーバプロビジョニング事情

インフラ部の荒井(@ryot_a_rai)です。この記事ではクックパッドで利用しているプロビジョニングツール "Itamae" の紹介と細々した Tips を紹介します。

式年遷宮とプロビジョニングツール

現在、弊社ではインフラの式年遷宮*1を進めています。式年遷宮以前、弊社では Puppet を利用してサーバをセットアップしていましたが、式年遷宮に際して既存のプロビジョニングに関するコードは捨てることになるため、プロビジョニングツールの再検討を行うことになりました。

Puppet, Chef, Ansible, SaltStack を検討した結果、

  • 言語特性の観点では、Ruby DSL な Chef が良い
  • アーキテクチャ・エコシステムの観点では、シンプルな Ansible が良い

といった点から、どれも決め手に欠ける状況で、Ruby DSL で記述できるシンプルなプロビジョニングツールが必要とされていました。 そこで、以前から筆者が細々と開発していたItamae(当時は Lightchef と呼んでいました)が採用され、開発が進められました。

Itamae とは

Itamaeは一言で言うとかなりシンプルな Chef で、以下のような特徴があります。

  • Chefにおける cookbook, role, environment などの概念はなく、レシピのみを管理します
    • 低い学習コストで使うことができます
  • プロビジョニング対象のサーバに Itamae が入っている必要がない
    • SSH 経由で他サーバをプロビジョンすることが可能です
  • Gem でプラグインを作ることができる
    • Bundler のみで依存関係を記述することができる
    • Chef における Berkshelf の代替

入門 Itamae

Itamae の使い方について軽く触れてみます。

まず、Ruby と Bundler をインストールしておいてください。作業用ディレクトリを作ります。

$ mkdir itamae-getting-started
$ cd itamae-getting-started

Itamae をインストールするために Gemfile を置きます。直接gem installでインストールすることも可能ですが、レシピが意図せず動かなくならないように Bundler で Itamae のバージョンを固定することをおすすめします。プラグインを使う場合には、この Gemfile にプラグインを追加するだけで利用できます。

# Gemfile
source 'https://rubygems.org'
gem 'itamae'

Itamae をインストールします。

$ bundle install

レシピを書いてサーバの状態を記述することができます。ここでは sl コマンドをインストールしてみます。

# sl.rb
package "sl" do
  action :install # デフォルト値なので省略可能
end
$ bundle exec itamae local sl.rb
 INFO : Starting Itamae...
 INFO : Recipe: /home/ryotarai/itamae-getting-started/sl.rb
 INFO :   package[sl] installed will change from 'false' to 'true'

なお、SSH 越しに実行する場合は以下のように実行します

$ bundle exec itamae ssh --host host001.example.jp sl.rb

上記のレシピに書かれているpackageはリソースと呼ばれ、サーバ上の何かしらのリソース(パッケージやファイルなど)の状態を記述します。Itamae には他にも様々なリソースが用意されていますが、代表的なものをいくつか紹介します。

# package リソース
package "nginx" do
  action :install
  version "..."
end

# remote_file リソース
# ファイルを特定のパスに置くことができます
remote_file "/etc/nginx/nginx.conf" do
  source "nginx.conf"
end

# template リソース
# remote_file リソースと同様ですが、eRuby (ERB)として評価した結果を書き出します
template "/etc/nginx/conf.d/itamae" do
  source "itamae.erb"
end

# execute リソース
# 任意のコマンドを実行することができます
execute "echo Hello >> /etc/something" do
  not_if "grep Hello /etc/something" # このコマンドが失敗した場合のみ実行されます
  # only_if "grep -v Hello /etc/something" # このコマンドが成功した場合のみ実行されます
end

ほかにもリソースが用意されているので一度 https://github.com/itamae-kitchen/itamae/wiki/Resources に目を通してみることをおすすめします。

もう一つよく使われる機能として、レシピから他のレシピを読み込む機能があります。

include_recipe "sl.rb"

上記のように、include_recipeに読み込みたいレシピのパスを渡すと他のレシピを読み込むことができます。パスはinclude_recipeを書いたレシピがあるディレクトリからの相対パスになります。 ちなみに、同じレシピを複数回include_recipeしても1度しか読み込まれないようになっているので、ご注意ください。

基本的な使い方は以上です。これだけ覚えれば使い始められるので、ぜひ導入してみてください。

さらに詳しい使い方などはドキュメントを参照してください。

Itamae Tips

クックパッドでの Itamae の使い方で特徴的な点をいくつか紹介します。

role、cookbook

前述の通り、Itamae には role や cookbook を管理する仕組みはありませんが、include_recipeで他のレシピを読み込むことで同様の機能を実現しています。

例えば、

├── bootstrap.rb
├── cookbooks
│   ├── nginx
│   │   ├── default.rb
│   │   └── templates
│   │       └── etc
│   │           └── nginx
│   │               └── nginx.conf.erb
│   └── ruby
│       └── default.rb
└── roles
    └── web
        └── default.rb

このように、cookbook と role のディレクトリを用意し特定の命名規則にしたがってファイルを置いています。

# bootstrap.rb
# 2015/05/12 10:03 修正
module RecipeHelper
  def include_cookbook(name)
    include_recipe File.join(__dir__, "cookbooks", name, "default.rb")
  end
end

Itamae::Recipe::EvalContext.include(RecipeHelper)

include_recipe File.join("roles", node[:role], "default.rb")
# roles/web/default.rb
include_cookbook "nginx"
include_cookbook "ruby"

role はサーバによって異なるので、node[:role]を参照して実行するようにしています。クックパッドでは EC2 インスタンスのタグで role を指定しているので specinfra-ec2_metadata-tags を利用して、実行するレシピを決定しています。

このように準備しておくと、以下のように実行することができます。

$ echo '{"role": "web"}' > node.json
$ bundle exec itamae local --node-json=node.json bootstrap.rb

remote_file, templateのsource :auto

remote_file, template リソースにはsourceアトリビュートがあり、これでファイルやテンプレートを指定します。

$ ls
recipe.rb   nginx.conf.erb
# recipe.rb
template "/etc/nginx/nginx.conf" do
  source "nginx.conf.erb"
end

通常、sourceにはファイルパスを指定しますが、特別な値として:autoが用意されています。:autoが指定されると、Itamaeは配置先のファイルパスから自動的にファイルを探します(詳細)。ちなみに、sourceのデフォルト値は:autoなので、これは省略することができます。

├── recipe.rb
└── templates
    └── etc
        └── nginx
            └── nginx.conf.erb
# recipe.rb
template "/etc/nginx/nginx.conf" do
end

この方法を利用すると、ディレクトリ構成がわかりやすくなりファイルが増えた場合にも管理しやすくなると感じています。

Node#reverse_merge!

ノードアトリビュートにデフォルト値を設定する場合は、Node#reverse_merge!が便利です。Node#reverse_merge!はすでに値がセットされている場合は上書きせず、ディープマージを行います。

例えば、以下のように使います。

# recipe.rb
node.reverse_merge!({
  nginx: {
    worker_processes: 4
  }
})

template "/etc/nginx/nginx.conf"
$ bundle exec itamae local recipe.rb
# この場合、worker_processesは4になります

$ echo '{"nginx": {"worker_processes": 8}}' > node.json
$ bundle exec itamae local --node-json=node.json recipe.rb
# この場合、worker_processesは8になります

SSHを使わない

Itamae はコマンドを実行して、その結果を受け取ってから、次のコマンドを実行するため、レイテンシが高いサーバに対して SSH 実行を行うと、遅く感じることがあります。そのような場合は、対象サーバに Itamae をインストールすることで解消します。クックパッドでも最初は SSH 実行を使っていましたが、国外のサーバが増えるにつれ、レイテンシが気になるようになりサーバ上でのローカル実行に切り替えました。

システムに入っている Ruby を使って Itamae をインストールすることも可能ですが、Ruby のバージョンアップなどによって Itamae が動かなくなってしまうことを防ぐため、Ruby などの依存関係ごとインストールするパッケージを用意しています。現在、Ubuntu 14.04 用の Debian Package のみビルドしていますが、chef/omnibus を使っているので、他ディストリビューション用のパッケージもビルドできると思います。

オペレーションフロー

実際に Itamae を実行する際には複数台にまとめて実行するため、Capistrano でレシピを転送したあと Itamae を実行しています。

ただ、この方法だと

  • 台数が増えた時に遅い
  • 台数が増えてくるとオペレーションのコストがかかる
  • 手動で実行していると、レポジトリ上のコードとサーバの状態が食い違う
    • コミットされたからといって Itamae が実行されるわけではない

といった問題があり、現在自動実行の仕組みを開発中です。

f:id:ryotarai:20150511221427p:plain

GitHubへのプルリクエストの作成やプッシュを契機として、dry-run や実際の実行を行います。上図の通りクックパッドでは Consul を利用しようとしていますが、Itamae の SSH 実行など他のバックエンドも用意しようと考えています。

今後の方向性

今後も低い学習コストで使い始められ、軽量・シンプルに使えるという特徴を維持していきます。 それと同時に、大きな規模になっても使える機能を備えていきたいと考えています。

欲しい機能がある場合やバグを見つけた場合は、遠慮なく Issue や Pull Request を作成していただければと思います。

Itamae Meetup is coming soon

ついに実用段階に入った、と言っても過言ではない Itamae ですが、利用事例も少しずつ聞くようになってきたのでミートアップを開催する予定です。日時などはまだ未定ですが、ぜひお越しください&発表してください。