weblog of key_amb

主にIT関連の技術メモ

ちょっとしたワークフローエンジンを作るときは Rake で十分だと思う

Ruby なら特に。

Ruby 以外でも、使えるケースはありそう。

Rake には次のような機能が有る。

  • タスクの依存関係定義
  • 並列実行
  • 別のタスクの呼び出し

また、タスクを記述する Rakefile 内では Ruby の文法が使えるので、特に外部の gem を使わなくても、任意のタスクにアドオンで次のような機能を付加できる。

  • リトライ
  • 終了処理
  • 繰り返し
  • 複数のタスクを並列実行

以下にサンプルの Rakefile を示す。
※ここでは parallel を使っているが、同じことを parallel を使わずに記述することもできる。

require 'parallel'

def invoke(task)
  Rake::Task[task].invoke
end

def execute(task)
  Rake::Task[task].execute
end

def parallel(*tasks)
  Parallel.each(tasks) do |task|
    invoke(task)
  end
end

task :all do
  invoke :prepare
  begin
    invoke :start
    begin
      invoke :main
      parallel :job_foo, :job_bar
      3.times { |i| p i; execute :job_to_iterate }
      invoke :finalize
    end
  ensure
    invoke :clean_up
  end
end
task default: :all

task :prepare do
  p :prepare
end

task :start do
  p :start
end

task :main do
  p :main
end

task :job_foo do
  p :job_foo
end

task :job_bar do
  p :job_bar
end

task :job_to_iterate do
  p :job_to_iterate
end

task :finalize do
  p :finalize
end

task :clean_up do
  p :clean_up
end

実行すると、次のようになる。

% rake --trace
** Invoke default (first_time)
** Invoke all (first_time)
** Execute all
** Invoke prepare (first_time)
** Execute prepare
:prepare
** Invoke start (first_time)
** Execute start
:start
** Invoke main (first_time)
** Execute main
:main
** Invoke job_foo (first_time)
** Invoke job_bar (first_time)
** Execute job_foo
** Execute job_bar
:job_foo
:job_bar
0
** Execute job_to_iterate
:job_to_iterate
1
** Execute job_to_iterate
:job_to_iterate
2
** Execute job_to_iterate
:job_to_iterate
** Invoke finalize (first_time)
** Execute finalize
:finalize
** Invoke clean_up (first_time)
** Execute clean_up
:clean_up
** Execute default

この Rakefile を見れば、タスクのワークフロー制御がどう記述されているか、プログラマならすぐ理解できるだろう。

今回の :all のような、ユーザが実行するワークフローを記述したタスクは、外部から直接利用されるメインのタスクと言えるだろう。
メインタスクから呼び出される個々のタスクは、サブタスクということになる。

メインタスク内では、ワークフロー制御と、サブタスクの呼び出しのみを記述すべきだ。
細かな内部ロジックはサブタスク側に記述することで、 Rakefile や実装されているワークフローを見通しよく保つことができるだろうと期待される。

参考

Ginza.rb で "grifork" について発表してきた

10/18(火)に、第40回の Ginza.rb に参加してきました。

初参加でしたが、今回は他にも初参加の方が8名ほど(?)いらしていたようです。

Ginza.rb では、毎回、別々のテーマについて会を催しているようです。
今回は「自分でつくったものを見せてみよう」というテーマでした。

約17名*1の参加者中、ほぼ全員が発表者だったため、1人あたりの発表時間が4分ほどになりました。
LT より短いレベルですね^^;

私は、前記事で紹介した "grifork" について発表してきました。
下がそのスライドです。

だいたい前の記事で紹介した内容の通りですが、v0.2 〜 v0.5 のアップデートについても触れています。
この場でも、かんたんに述べておきます:

  • v0.3 gem 化しました。 https://rubygems.org/gems/grifork
  • v0.4 Parallel, ssh, rsync のオプションを DSL でカスタマイズできるようにしました。
  • v0.5 prepare, finish という構文でジョブの最初と最後に実行するタスクを記述できるようにしました。

発表後、Twitterハッシュタグを見ていたところ、質問らしいツイートがありました。

ごもっともな質問です。
今はある意味、「何もしてない」のですが、エラーがあったらそこでジョブ全体が異常終了するようになってます。

経験上、あるノードでデプロイが止まる場合、そのノードが異常な状態であることが多いと思います。
ので、その場合、そのノードを取り除いて再デプロイする、というのがよくある運用手順になるかな、と思います。*2

grifork の仕組みとして、リトライを入れたり、異常ノードを正常ノードと入れ替えてなるべく多くのノードに対してタスクが完了するように仕向ける、といった実装も可能とは思いますが、その辺りは要望があったら考えようかなぁ、というところです。
何かご意見などありましたら、 GitHub Issue でも Twitter でもお気軽にお寄せ下さい。

自分以外の発表について

一部 Ruby じゃなかったりもあった気がしますが、発表された成果物がバリエーションに富んでいて、刺激を受けました。

特に @joker1007 さん作のものには、ふだんお世話になっているものもありそうです。
rukawa というワークフローエンジンがなぜ rukawa なのか気になりましたが、ちょっと風邪気味だったので懇親会は遠慮しました。

Ginza.rb について

自分がこれまで参加してきた技術系の勉強会は、メインパートが「発表( + LT + 質疑)」で、その後、懇親会という流れのものが多かったです。
が、Ginza.rb は前の回の振り返りや次回内容の検討があって、新鮮でした。

またの機会がありましたら、よろしくお願いします。

ありがとうございました!

*1:エントリーしていた方は、見たところほぼ全員出席していたと思います

*2:Twitter でも同じように回答しました。

ssh と rsync だけで Tree Deploy を実現する "grifork" を作った

はじめに〜fireap to grifork

約半年前に fireap というデプロイツール(タスクランナー)を作りました。

オンプレミスでも使えて、ノード数 N に対して O(log N) で動作する、というものです。
が、前提条件として、システム内の全ホストに fireap をデプロイし、また、全ホストで Consul の agent を動かす必要があります。
その辺りが導入障壁になる環境もあるかもしれないな、と思いました。

…で、少し工夫すれば、「デプロイサーバにだけプログラムがあればツリー状にデプロイできる」ものも作れるだろう、と思って作ってみたのが、今回の grifork になります。

Ruby で書いてます。

Tree Deploy とは

なんとなく言葉の雰囲気で意味が伝わる気もしますが、grifork のメカニズムの説明がてらに Cacoo で絵を描いてみました。

f:id:key_amb:20161002145457p:plain

対象のホストリストをツリー状のグラフにマッピングして、デプロイをキックするサーバ(以下、デプロイサーバ)から樹状にデプロイを伝播していくと、そういうデプロイのやり方を意図しています。*1

fireap 同様、実行時間は O(log N) になります。

grifork: standalone モード

grifork には2つ動作モードがありますが、"standaloneモード" が、デプロイサーバでだけ grifork が動けばいいというモードです。

上図のようなツリーに対して、このモードでデプロイを行うと、下の3段階でデプロイが進行します。

(1) デプロイサーバから子ノードに rsync

f:id:key_amb:20161002151648p:plain

(2) デプロイサーバから第1世代の子ノードに ssh して、第2世代の子ノードへの rsync をキック

f:id:key_amb:20161002151700p:plain

(3) 以下、グラフの葉に達するまで繰り返し

f:id:key_amb:20161002151712p:plain

各世代のジョブは全て Parallel *2 で並行に走らせることにしました。
ので、最大並列数が「最大のノードを持つ世代のノード数」になります。

※まだ最大14ノードでしか試してないのですが、ひょっとしたら並列数がすごく大きくなったら何か問題出てくるかも。

grifork: grifork モード

もう1つの動作モードは "griforkモード" と呼んでいます。
こちらは、全ノードで grifork プログラムが動作する前提になっています。

このモードでは、子ノードで grifork を実行することで、再帰的に葉に達するまでタスクを実行します。

こちらの仕組みについても、先ほどのツリーを使って図解します。

(1) デプロイサーバから子ノードに rsync

※ standalone モードと同じなので、図は割愛

(2) 子ノードに ssh して各サブツリーへの grifork をキック

f:id:key_amb:20161002152223p:plain

(3) 以下、再帰的にグラフの葉に達するまで繰り返し

f:id:key_amb:20161002152251p:plain

standalone モードで grifork や ruby を配布して、grifork モードでアプリケーションをデプロイする、というやり方もできます。
grifork モードの実験をやるときは、実際に standalone モードで全ホストに grifork を配布していました。

使い方

まだ gem にしてないので、今のところ github から clone して使う形になります。

git clone https://github.com/key-amb/grifork.git
cd grifork
bundle install
./bin/grifork [--[f]ile path/to/Griforkfile]

デプロイツールとして紹介してきましたが、デプロイに限らずリモートでタスクを実行させる用途でも使えます。

Griforkfile という DSL ファイルにタスクの設定を書きます。
デフォルトでカレントディレクトリの Griforkfile を参照します。

リポジトリexample ディレクトリに、各モードの Griforkfile のサンプルを置いておきました。

動作例と実行ログ

ちょっとしたサンプルとそのログを Gist に貼っておきました。もし、参考にしたい方はどうぞ。

https://gist.github.com/key-amb/94b8113e315ed1ea0e0d98dd6071a9a9

今後の展望

とりあえず動くものはできた、というレベルなので、まだまだ色々使いづらいところや、エラーケースの対処など漏れているところもありそうです。

gem 化した方が使いやすそうだなと思いつつ、まだやっていません。

また、Go で書き直したら、grifork モードのために grifork 自身を配布するのも楽だろうな、と思っています。やる気次第だけど、できれば、やりたい。

以上、 sshrsync だけでクラスタに対してツリー状にタスクを伝播するタスクランナー "grifork" を紹介しました。

ご興味ありましたら、お試し下さい。

もし何か要望やバグなどありましたら、GitHub Issue か Pull Request か、Twitter などでお知らせ下さい。

余談〜デプロイの未来について

そもそもの話になりますが、PaaS やコンテナの普及によって、サーバアプリケーションをデプロイする仕組みは変わりつつあるように思います。
単純にファイルツリーを配布するようなデプロイツールのニーズは、相対的に下がっていきそうな気もしています。

おまけ〜grifork の語源

graph + fork (+ griffin)

"clenv" がそこそこ xxenv っぽく使えるようになってきた

3日前に上の記事を書いたばかりですが、また少し工事をしたので、自分の中での整理も兼ねて、お知らせ。

"clenv" は私が趣味で作っているもので、シェルスクリプトのパッケージ管理ツールのようなものです。

ソースコードhttps://github.com/key-amb/clenv で公開しています。

追加・変更したコマンドについて

clenv help の実行結果から抜粋します:

clenv init [-]             # Bootstrap
clenv create [ENVIRONMENT] # Initialize ENVIRONMENT
clenv environ              # Show current environment
clenv environs             # List environments
clenv global [ENVIRONMENT] # Get/Set global environment
clenv local  [ENVIRONMENT] # Get/Set local environment
clenv exec   EXECUTABLE    # Execute EXECUTABLE in clenv
clenv which  EXECUTABLE    # Show path of EXECUTABLE in clenv

exec, which は v0.3.0 で、後は v0.5.0 以降で追加しました。
clenv create は v0.4 以前の clenv init-env 相当、 clenv environsclenv-list-env 相当です。

clenv environ[s] は rbenv 等の version[s] サブコマンド相当で、現在の clenv 的な意味の environment を表示します。

コマンド動作イメージ

上のコマンド群を動かした場合の挙動の例を、軽く以下に示します:

% pwd
/home/key-amb/tmp
% clenv environ
default (set by /home/key-amb/.clenv/environment)
% clenv environs
* default
% clenv create foo          
Initialized env 'foo'
% clenv create bar
Initialized env 'bar'
% clenv environs
  bar
* default
  foo
% clenv local foo
% clenv environ
foo (set by /home/key-amb/tmp/.clenv-environment)
% CLENV_ENVIRONMENT=bar clenv environ
bar (set by CLENV_ENVIRONMENT)
% clenv global
default (set by /home/key-amb/.clenv/environment)

create 〜 install, environ[s] 〜 version[s] と考えれば、 rbenv 等に慣れた人であれば、そこそこ違和感なく使えるのではないでしょうか。

初期セットアップの改善

clenv のセットアップ方法も、より xxenv に近づき、以前よりシンプルになりました。
README から抜粋します。

# .bash_profile 等に追記する
export CLENV_ROOT=$HOME/.clenv
export PATH="$CLENV_ROOT/bin:$PATH"
eval "$(clenv init -)"

rbenv や plenv の system に相当するものがないので、 clenv init - の中で、初回は default という環境を create して global にセットすることにしました。

終わりに

clenv の主に v0.4 〜 v0.6 の変更について、ダイジェストでお伝えしました。

よろしければ、手元に入れて遊んでみて下さい。

Enjoy!

過去の記事

シェルスクリプト用テストツール "shove" v0.8 までの更新のお知らせ

シェルスクリプト用のテストツール "shove" を作って、初めて上の記事で紹介したのは約5ヶ月前になります。

今回は、上記事の時点からこれまでの主な差分をお知らせします。
言うなれば CHANGELOG + αな記事となります。

shove は GitHub で公開しています。URL は下になります:

https://github.com/key-amb/shove

CONTENTS:

グルーピングの新しい記法を追加 (v0.8.1)

今まで T_SUB "..." (( ... )) という記法でテストコードをグループ化できるようにしていました。
が、なんか微妙だなと思って t::group "..." ({ ... }) という新しい記法を追加しました。

なんか微妙だな、と思った以上に深い理由はあまりなかったりもします^^;
が、2つの記法をサポートするのはコード上、厳しいことになっていたりもするので、古い記法はその内消そうと思ってます。
ので、もし使っている人がいたら新記法に移行をお願いします。(すいませんすいません)

サンプルコードは README を更新していますので、そちらをご覧くださいませ。

テスト用の関数を12個追加 (v0.8.0)

コードをリファクタしていじりやすくなったので、調子に乗って色んな test 関数のオプションでテストできるように t_xxx な関数を追加しました。

README そのままですが、追加した関数とその使い方は下のようになります:

t_present   $str  "str is present"    # [ -n "$str" ]
t_blank     $str  "str is blank"      # [ -z "$str" ]
t_exist     $path "path exists"       # [ -e "$path" ]
t_file      $path "path is file"      # [ -f "$path" ]
t_directory $path "path is directory" # [ -d "$path" ]
t_symlink   $path "path is symlink"   # [ -L "$path" ]
t_eq        $x $y "x == y"            # [ $x -eq $y ]
t_ne        $x $y "x != y"            # [ $x -ne $y ]
t_gt        $x $y "x >  y"            # [ $x -gt $y ]
t_ge        $x $y "x >= y"            # [ $x -ge $y ]
t_lt        $x $y "x <  y"            # [ $x -lt $y ]
t_le        $x $y "x <= y"            # [ $x -le $y ]

右部のコメントの通りに test 関数が実行され、結果が成功ならテストが通ります。

shpec のテストを追加 (v0.7.2)

テストツールのテストを自分自身でやるのは微妙だな、という気はします。

shpec *1 は上の記事でもちょこっと紹介していますが、RSpec ライクな記法でシェルスクリプトのテストを可能にしてくれるツールです。

CI には入れてませんが、テストコードを少し書いて、ときどき手元で実行しています。

shpec では describe ... endit ... end でブロックを作ることができます。が、 shove のグルーピングのようにサブシェルは使っていないようでした。
単に RSpec っぽく書けて、結果もきれいに出せるもの、という理解でいいのかな、と思っています。

コードがミニマルな感じですごくきれいなので、 shove のリファクタで参考にさせてもらいました。

その他にも、少し内部構造をいじったり、ちょっとした改善がいくつか入っていたりします。

シェルスクリプトのテストをしたいシーンがありましたら、shove のことを思い出していただければ幸いです。

Enjoy!