前回はノード属性を使って汎用的なレシピを書く方法を紹介しました。汎用的なレシピは様々な環境で利用できるので、
プラグイン
Itamaeにはプラグイン機構が用意されていて、
レシピプラグイン
レシピプラグインを作ることで、itamae-plugin-recipe-(レシピ名)
の形式にする必要があります。rubygems.itamae-plugin-recipe
を検索すると既存のプラグインを探すことができます。
ここでは例としてnginx用のレシピプラグインを作ってみたいと思います。まず、bundle gem
コマンドを使って、
$ bundle gem itamae-plugin-recipe-nginx
gemspecファイルにTODO
が含まれていると、
レシピはlib/
に書くことで、include_
で読み込むことができます。また、lib/
のように同じディレクトリにレシピを配置すると、include_
またはinclude_
で読み込むことができます。
lib/itamae/plugin/recipe/nginx/default.rb
:node.reverse_merge!(
nginx: {
user: 'www-data',
worker_processes: 4,
}
)
package 'nginx' do
version node['nginx']['version'] if node['nginx']['version']
end
service 'nginx'
template '/etc/nginx/nginx.conf' do
owner 'root'
group 'root'
mode '644'
notifies :reload, 'service[nginx]'
end
lib/itamae/plugin/recipe/nginx/templates/etc/nginx/nginx.conf
:user <%= node['nginx']['user'] %>;
worker_processes <%= node['nginx']['worker_processes'] %>;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
レシピやテンプレートの書き方は通常のレシピと同様です。できるだけ様々なシーンで利用できるように、node
の値を参照して柔軟に設定できるようにしておくことをお勧めします。このレシピが期待通り動くかどうかをServerspecでテストしましょう。Serverspecはサーバの状態を記述して想定通りの状態になっているかどうかをテストするためのフレームワークです。
まず、
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec"
+ spec.add_development_dependency "serverspec"
end
テストはVagrantで起動した仮想マシンに対して実行するので、Vagrantfile
を生成します。なお、
$ vagrant init -m ubuntu/trusty64
Serverspec用の設定を書きます。spec/
に以下を記述します。
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'serverspec'
require 'net/ssh'
require 'tmpdir'
set :backend, :ssh
if ENV['ASK_SUDO_PASSWORD']
begin
require 'highline/import'
rescue LoadError
fail "highline is not available. Try installing it."
end
set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
else
set :sudo_password, ENV['SUDO_PASSWORD']
end
# Vagrantを使って仮想マシンを起動します
system "vagrant", "up", "default"
ssh_options = nil
config = Tempfile.new("itamae")
config.write(`vagrant ssh-config default`)
config.close
ssh_options = Net::SSH::Config.for("default", [config.path])
config.unlink
ssh_options[:user] ||= Etc.getlogin
set :host, ssh_options[:host_name]
set :ssh_options, ssh_options
Serverspecのテストを記述します。
require 'spec_helper'
require 'itamae'
describe Itamae::Plugin::Recipe::Nginx do
before(:all) do
Itamae.logger.level = ::Logger::INFO
# SSHでVagrantのホストに対してItamaeを実行します
backend = Itamae::Backend.create("ssh", {vagrant: true, sudo: true})
backend.run_command(%w!apt-get update!)
runner = Itamae::Runner.new(backend, {})
runner.load_recipes([File.expand_path("lib/itamae/plugin/recipe/nginx/default.rb")])
runner.run(dry_run: false)
end
# nginxパッケージがインストールされていること
describe package('nginx') do
it { is_expected.to be_installed }
end
# nginxサービスが実行中であること
describe service('nginx') do
it { is_expected.to be_running }
end
# nginxのワーカプロセスが4つ起動していること
describe command('ps aux | grep [n]ginx | grep worker | wc -l') do
its(:stdout) { is_expected.to eq("4\n") }
end
end
BundlerでServerspecをインストールし、
$ bundle install $ bundle exec rake spec (中略) Itamae::Plugin::Recipe::Nginx INFO : Recipe: /Users/ryota-arai/src/github.com/ryotarai/itamae-plugin-recipe-nginx/lib/itamae/plugin/recipe/nginx/default.rb Package "nginx" should be installed Service "nginx" should be running Command "ps aux | grep [n]ginx | grep worker | wc -l" stdout should eq "4\n" Finished in 16.68 seconds (files took 7.82 seconds to load) 3 examples, 0 failures
ItamaeがVagrantの仮想マシンに実行された後、rake release
でリリースします。
$ bundle exec rake release
リソースプラグイン
続いて、itamae-plugin-resource-(リソース名)
にします。ここでは例としてcron
リソースを作ってみます。レシピプラグインと同様にbundle gem
コマンドでひな形を作成します。
$ bundle gem itamae-plugin-resource-cron
lib/
を編集してリソースを実装します。まず、
require "itamae"
module Itamae
module Plugin
module Resource
class Cron < ::Itamae::Resource::Base
class Error < StandardError; end
# define_attributeでリソースで使える属性を宣言します
# デフォルトのアクションはcreate
define_attribute :action, default: :create
define_attribute :minute, type: String, default: '*'
define_attribute :hour, type: String, default: '*'
define_attribute :day, type: String, default: '*'
define_attribute :month, type: String, default: '*'
define_attribute :weekday, type: String, default: '*'
define_attribute :cron_user, type: String, default: 'root'
define_attribute :command, type: String
# default_name: trueを指定すると、その属性が指定されなかった場合にリソース名が使われます
define_attribute :cron_name, type: String, default_name: true
# pre_actionはアクション前に実行されるメソッドです
def pre_action
case @current_action
when :create
# createアクションの場合、cronリソースが存在する(exist == true)状態になることが期待されている
attributes.exist = true
when :delete
# deleteアクションの場合、cronリソースが存在しない(exist == false)状態になることが期待されている
attributes.exist = false
end
end
# set_current_attributesでホスト上の現在の状態を設定します(アクション前に実行される)
def set_current_attributes
if run_specinfra(:check_file_is_file, cron_file)
current.exist = true
# 既存のcron設定をパースする
fields = parse_crontab(backend.receive_file(cron_file))
current.minute = fields[:minute]
current.hour = fields[:hour]
current.day = fields[:day]
current.month = fields[:month]
current.weekday = fields[:weekday]
current.cron_user = fields[:cron_user]
current.command = fields[:command]
else
current.exist = false
end
end
private
def cron_file
key = attributes.cron_name.gsub(%r{(\s+|/)}, '-')
"/etc/cron.d/itamae-#{key}"
end
def parse_crontab(crontab)
line = crontab.each_line.find {|l| !l.start_with?('#') }
r = line.chomp.match(/\A([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)\z/)
unless r
raise Error, "Invalid crontab format."
end
{minute: r[1], hour: r[2], day: r[3], month: r[4], weekday: r[5],
cron_user: r[6], command: r[7]}
end
end
end
end
end
基本的な動作はコード内のコメントに記述しましたが、
set_
でホスト上の現在のリソースの状態current_ attributes (current) を読み込む pre_
でその他の処理を実行するaction current
(現在の状態) と attributes
(設定したい状態) の差を埋める
といった形です。current
とattributes
を適切に設定しておくと、current
とattributes
に差がある場合のみ、notifies
とsubscribes
に指定したアクションが実行されます。
実際のアクションはaction_(アクション名)
メソッドに実装します。cronリソースの場合は以下のように実装しました。
module Itamae
module Plugin
module Resource
class Cron < ::Itamae::Resource::Base
# (中略)
def action_create(options)
# ローカルの一時ファイルにcron定義を書く
f = Tempfile.open('itamae')
f.write(generate_cron)
f.close
# 対象サーバの一時ファイルに送信
temppath = ::File.join(runner.tmpdir, Time.now.to_f.to_s)
# send_fileを呼ぶと、ローカルから対象サーバにファイルが転送されます
# (例えば、SSH実行の場合、SCPで送信されます)
backend.send_file(f.path, temppath)
# 一時ファイルを動かす
# (一旦、一時ファイルに書いているのは、アトミックにファイルを更新するため)
run_specinfra(:move_file, temppath, cron_file)
ensure
f.unlink if f
end
def action_delete(options)
if current.exist
run_specinfra(:remove_file, cron_file)
end
end
private
def generate_cron
<<-EOCRON
# DO NOT EDIT THIS MANUALLY
# BECAUSE THIS IS AUTO GENERATED BY Itamae
#{attributes.minute} #{attributes.hour} #{attributes.day} #{attributes.month} #{attributes.weekday} #{attributes.cron_user} #{attributes.command}
EOCRON
end
# (中略)
end
end
end
end
ここまで何回か登場したrun_
はSpecinfraのコマンドを実行するためのメソッドです。ItamaeはSpecinfraの上で動く設計になっていて、run_
を呼ぶと対象サーバのOSなどを判定して適切なコマンドを実行してくれます。例えば、install_
を呼ぶとUbuntuではapt-get install
、yum install
を実行します。この仕組みを利用することでItamae側にはOSごとの分岐をほぼ書かずに実装ができています。実際にどういったコマンドが利用できるかは、
では、
# Gemfile
source "https://rubygems.org"
gem "itamae-plugin-resource-cron", path: "/path/to/itamae-plugin-resource-cron"
# example.rb
cron 'say hello' do
minute "10"
command 'echo Hello'
end
Bundlerを使って実行します。
$ bundle install $ bundle exec itamae local example.rb (略) $ cat /etc/cron.d/itamae-say-hello
/etc/
の下にファイルができていれば成功です。なお、
まとめ
今回はレシピをプラグインとして再利用可能な形で公開する方法、