log.fstn

技術よりなことをざっくばらんにアウトプットします。

Jenkinsの無秩序なジョブをDigdagで再定義する

ということで Jenkins のジョブを Digdag に置き換えて Git で管理すると最高なので、今困っている人はやりましょう。1日あれば多分終わります。 今回試したのは CI のジョブですが、どんなジョブでも応用できると思います。

詳しく

こないだ Rebuild 152 聴いていたらその会話の中に「Jenkinsおじさん」ってワードが出てきたんですよ。

rebuild.fm

Jenkinsをそれなりの規模で使っている人ならお馴染みだと思うんですが、Jenkinsって自由度が高くてジョブの編集も簡単にできるから気をつけないとジョブがカオスな状態になってきて、 管理できる人が限られてしまいます。ジョブを編集したいと思ったらその「管理している人」に許可を取って、これでいいですかね?ってジョブの内容を確認してもらってWeb上から直接ジョブを書き換えると… いわゆるこの「管理している人」が「Jenkinsおじさん」なわけですが、この管理方法だとJenkinsおじさんにとってもその他の利用者にとってもジョブの編集が怖くなってくるんですよね。 あと、経験上こういうフローで編集したジョブってだいたい初回はちゃんと動かなくて結局みんなに迷惑かけちゃいます。つらい。

脱 Jenkins おじさん

まずは Git で管理

とりあえずジョブは Git で管理できるようにして、GitHub とかでちゃんと Pull Request 作ってレビューしてもらう体制を整えたいです。Git で管理するようにすれば過去の変更は全て追えるようになるし、何か問題が起きたときは特定しやすくなります。Pull Request を利用したレビューも普段から使い慣れているので同じようにやるだけですね。

これだけでカオスな状態になりにくい環境になると思います。

次に肝心のジョブの定義についてですが、いくつか方法があると思います。

現在のジョブをそのままファイルに書き出す

レポジトリにシェルスクリプトのファイルを置くだけです。 あとは Jenkins ジョブ上でこのファイルを実行すればOK。 単純でかつ簡単にできますが、そもそもシェルスクリプトの内容が複雑になってきてそろそろ書き換えたかったので今回はパス(シェルスクリプトが苦手ってのもある)。

Jenkins2 にして Jenkinsfile を記述する

Jenkins2には既にしてあったので最初はこれが最有力な選択肢でした。 ただ検証してみたら Jenkinsfile を書くための学習コストが結構高かったのと、GitHub pull request builder plugin が Pipeline ジョブに未対応( Jenkinsfile を使うには Pipeline への対応が必要)だったのでやめました。

GitHub pull request builder plugin 使わないのであれば、Pipeline、Multiple Pipeline、Github Organization あたりのジョブで Jenkinsfile が使えるので良いと思います。 またこれらのジョブはステップ毎の実行結果や実行時間を表示してくれていい感じなので試してみるのをおすすめします。 新しいUIとして Blue Ocean というものも出てきてるのでついでに見てみるのもいいです(現在はベータ版ですがプラグインとしてインストールできます)。

Digdag を使う

Rebuild 152 でも話題になってましたが Digdag が今回の用途としては筋が良さそうでした。 Digdag はざっくり説明するとシェルスクリプトPythonRubyなどで記述したタスクを yaml(正確には yaml を拡張した dig ファイル) で定義したフローで実行するシンプルなワークフローエンジンです。 詳しくはドキュメント読んだり、検索すれば関連記事が沢山出てくるのでそれらを参照してもらうと良いと思います。

Jenkins のジョブを Digdag で再定義する

対象のアプリケーションは Rails で、フロントエンドのテストとバックエンドのテストが独立しているとします。 また、バックエンドのテストの一環として RSpec の他に Rubocop を実行することとします。 なお、フロントエンドのテストは js ファイルが変更された場合、バックエンドのテストは rb ファイルが変更された場合のみ実行させるとします。

この場合 Digdag では以下のように定義することが出来ます。

test.dig

timezone: UTC

_export:
  rb:
    require: 'tasks/test'

+setup:
  rb>: Test.setup

+test:
  +frontend:
    if>: ${diff_js}
    _do:
      rb>: Test.frontend

  +backend:
    if>: ${diff_rb}
    _do:
      +setup:
        rb>: Test.setup_for_backend

      +backend_test:
        +execute_rubocop:
          rb>: Test.execute_rubocop

        +execute_rspec:
          rb>: Test.execute_rspec

下記はタスクの例です。

tasks/test.rb

class Test
  def setup
    Digdag.env.store(diff_js: diff?('.js'))
    Digdag.env.store(diff_rb: diff?('.rb'))
  end

  def frontend
    # 省略
  end

  def setup_for_backend
    run "bundle install"
  end

  def execute_rubocop
    # 省略
  end

  def execute_rspec
    run 'bundle exec rspec'
  end

  private

  def diff?(file)
    # 省略
  end

  def run(command)
    start_time = Time.now
    puts "run `#{command}`".blue
    system command
    finished_time = Time.now

    if $?.success?
      puts "finished `#{command}` in #{(finished_time - start_time).round 2} seconds".blue
    else
      raise "failed `#{command}` in #{(finished_time - start_time).round 2} seconds".blue
    end
  end
end

class String
  def blue; "\e[34m#{self}\e[0m" end
end

Jenkins側のジョブは

digdag run test.dig -a --project .jenkins

だけで大丈夫です( .jekinstest.dig がある場合)。

小ネタですが def run のところで実行時間を測っておくと

00:00:31.677 finished `bundle install` in 3.45 seconds

のように出力されるので便利です。

おわりに

Digdag でフローを定義することによってジョブの見通しがよくなりました。 Digdag なら Jenkins 以外の Circle CI や Travis CI などの SaaS でも普通に動くはずのなのでサービスの選択肢が広がっていいですね。

学習コストも低く1日あれば試せるのでJenkinsおじさん問題で困っている方は検討してみてはいかがでしょうか。