プロビジョニングツールをChef-soloからItamaeに替えてみました

Chef から Itamae ?

EY-Officeではサーバーソフトの構築を行うプロビジョニングツールとして現在は Chef-solo を使っています。しかし Chef-solo は終了するらしいのですが、その移行先が良いように思えず悩みながら使い続けてきました。

そんなとき登場してきた Itamae はシンプルでとても魅力を感じました。詳しくは クックパッド開発者ブログのItamae記事 をご覧下さい。

今回時間があったので、EY-Officeの開発支援サーバー (Redmine, Git/Gnatara, CI ...)の全プロビジョニングをChefからItamaeに置き換えてみました。書き換えたレシピは約700行です、そこで感じた事を書きます。

Itamaeの良いところ、良くないところ

1. 情報が貧弱

まず良くない点ですが、Chefのドキュメント に比べると情報の量、質ともに低いです。どうしても思ったように動作しない場合は Itamae の実装を読むことが度々ありました。

公式なドキュメントは

今回、参考になった情報は

2. Chefとは動作モデルが違うぞ!

最初に戸惑ったのでは、Chefとの動作モデルの違いです。

Chef-solo の利用イメージは

  1. Chefを開発環境にインストール
  2. 利用開始時にChefのRubyインタプリターを含む実行環境をターゲットにインストール (knife solo prepare コマンド)
  3. レシピの作成・修正
  4. 実行 (knife solo cooke コマンド)
    1. レシピの文法チェック等
    2. レシピを含むcookbookをターゲットへ転送
    3. レシピをターゲットで実行
  5. 問題があれば 3.に戻る

ですが、Itamaeは

  1. Itamaeを開発環境にインストール
  2. レシピの作成・修正
  3. 実行 (itamaeコマンド)
    1. レシピに対応する操作をsshを使いターゲット上で実行
  4. 問題があれば 2.に戻る

ChefのレシピのRubyコードはターゲット上で実行されますが、Itamaeはレシピは開発環境で実行されます。 したがって、ターゲットの状況によって処理を変更する部分、たとえば not_ifonly_if にはRubyのコードは書けず、shellのコードを書く必要があります。

Chef

execute "apt-get-update" do
  command "apt-get update"
  only_if { File.mtime('/var/lib/apt/lists') < Time.now - 86400 }
end

Itamae

execute "apt-get-update" do
  command "apt-get update"
  only_if "exit $(( `stat -c %Y /var/lib/apt/lists` > (`date +%s` - 86400) ))"
end

実行時にターゲット上の情報を動的に取得できないので、今回はレシピ(コード)で自動的に取得するのをあきらめnode で指定するようにした部分もありました。

3. Chefに似ているけど・・・

Itamae のResource (DSL, 命令?)はChefと似た作りになっていて、そのまま動くものもありますが、少し違う部分もあります。また無いものもあります。

  • Chef の cooke_fileremote_file
  • Chef の remote_file に相当するものは無い?
  • ファイル等のモード指定に8進数が使えない
  • useraction: :modify が出来ない
  • cron Resource が無い
  • ・・・

4. defineを使おう

無いResourceを簡単に作れる Definitions があり、これを使って足りないResourceを補いました。

# URL指定でネット上のファイルをダウンロードするResource
define :external_file, url: nil, owner: nil, group: nil, mode: nil do
    p = params

    execute "download #{p[:name]} from #{p[:url]}" do
        cmd =  "curl -s -f -L -o #{p[:name]} #{p[:url]}"
        cmd += " && chown #{p[:owner]} #{p[:name]}"  if p[:owner]
        cmd += " && chgrp #{p[:group]} #{p[:name]}"  if p[:group]
        cmd += " && chmod #{p[:mode]} #{p[:name]}"   if p[:mode]

      command cmd
      not_if "test -e #{p[:name]}"
    end
end

# crontab 設定用 Resource
define :cron_for, crontab: nil do
  user = params[:name]
  crontab = params[:crontab]
  crontab_path = "/tmp/crontab.#{rand(10000)}"

  file crontab_path do
    content crontab
    not_if "crontab -u #{user} -l"
  end

  execute "crontab -u #{user}" do
    command "crontab -u #{user} #{crontab_path} && rm #{crontab_path}"
    only_if "test -e #{crontab_path}"
  end
end

利用レシピ

ruby_package = "ruby-2.2.2"
external_file "/usr/local/src/#{ruby_package}.tar.bz2" do
  url "http://cache.ruby-lang.org/pub/ruby/2.2/#{ruby_package}.tar.bz2"
end

cron_for "root"  do
  crontab <<-EOH
#
PATH=/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
#
30 5 * * * /usr/local/bin/backup.sh >> /var/tmp/bakup.log  2>&1
  EOH
end

Chef の cron Resource は crontab形式を知らなくてもDSLで設定出来るようになっていましたが、define であまり高度な機能を作る事は考えない方が良いと思いました。やってみると直ぐに限界が見えます。

どうしても高度な Resourceが必要な場合は Quirra: itamae resourceの書き方 を参考に Resource を作った方が良いと思います。

5. シンプルで早い

動作モデルの違いやResourceの充実度の違いはありますが、Itamaeはレシピが実行されるまでの時間が早くとても快適です!

Chef-soloを利用していて、複雑化するChefの推奨移行環境に疑問を持つ方は、ぜひ一度 Itamaeを試してみると良いと思います。

今回作ったレシピ

今回作ったレシピの一部です(ubuntu 14.04用です)

execute "apt-get-update" do
  command "apt-get update"
  only_if "exit $(( `stat -c %Y /var/lib/apt/lists` > (`date +%s` - 86400) ))"
end


execute "ensure ja_JP.UTF-8" do
  command "locale-gen ja_JP.UTF-8"
  not_if "grep -c 'ja_JP.UTF-8' /var/lib/locales/supported.d/local"
end

execute "ensure JST" do
  command <<-EOH
echo "Asia/Tokyo" | sudo tee /etc/timezone
sudo dpkg-reconfigure --frontend noninteractive tzdata
  EOH
  not_if "date | grep -c 'JST'"
end

# shell を zsh に変更 (ユーザーは node[:user]で指定)
file "/etc/passwd" do
  action :edit
  block do |content|
    content.gsub!("/home/#{node[:user]}:/bin/bash", "/home/#{node[:user]}:/bin/zsh")
  end
end

postgreSQLをインストールし DBのencoding, localeを変更する

%w{postgresql-9.3 libpq-dev postgresql-client-9.3 postgresql-contrib-9.3}.each do |pkg|
  package pkg do
    action :install
  end
end

service "postgresql" do
  action [:start, :enable]
end

link "/usr/local/bin/psql" do
  to "/usr/local/pgsql/bin/psql"
end

execute "initdb" do
  user "postgres"
  cwd "/var/lib/postgresql/9.3/"
  command <<-EOH
rm -rf main
/usr/lib/postgresql/9.3/bin/initdb -D main --encoding=UTF-8 --locale=ja_JP.UTF-8
rm main/pg_hba.conf main/pg_ident.conf main/postgresql.conf
ln -s /etc/ssl/certs/ssl-cert-snakeoil.pem main/server.crt
ln -s /etc/ssl/private/ssl-cert-snakeoil.key main/server.key
EOH
  action :nothing
end

execute "re-initdb with ja_JP.UTF-8" do
  command "echo re-initdb"
  notifies :stop, 'service[postgresql]', :immediately
  notifies :run, 'execute[initdb]', :immediately
  notifies :start, 'service[postgresql]', :immediately
  not_if "/usr/lib/postgresql/9.3/bin/psql -U postgres -c \"select * from pg_database where datname ='template0'\" " +
    "| grep -c 'ja_JP.UTF-8'"
end

注) specinfra 2.41.1 で Ubuntu14.04でPostgreSQL起動中判定の問題が対処されたので暫定パッチは不要になりました :-)