digdag

digdag の rb オペレータで bundle exec を使う3つの方法

EDIT: 0.9.35 でruby オプションを指定できるようになったので追記


前提知識

ruby の前提知識および現在の digdag ができること、できないこと。

読み飛ばし可能。


digdag 0.9.33 から ruby へのパスを指定できるようになった

_export:

rb:
ruby: /path/to/ruby

もしくは

+task:

rb>: MyWorkflow.method
ruby: /path/to/ruby

しかし /path/to/bundle exec /path/to/ruby のような書き方はできない。また、できたとしても Gemfile が置いてあるパスに cd しなければならない


bundle exec は Gemfile をワーキングディレクトリに要求する

Gemfile, Gemfile.lock, .bundle/config がおいてあるディレクトリに cd して実行する必要がある。

cd /path/to/bundle

bundle exec ruby

しかし、digdag の rb オペレータには working directory を指定するようなオプションはない。


BUNDLE_GEMFILE 環境変数と -rbundler/setup

cd したくない(or できない)場合は BUNDLE_GEMFILE 環境変数で Gemfile のパスを指定して、bundler/setup を require すれば bundle exec と同じ効果を出せる。

FYI: bundle install --binstubs した bin スクリプトを読むとそんな感じの処理が書いてある

env BUNDLE_GEMFILE=/path/to/Gemfile ruby -rbundler/setup


RUBYOPT の解釈は ruby オプションより後

RUBYOPT=-rbundle/setup のように環境変数を設定すると ruby は -rbundle/setup オプションを指定したものとして扱ってくれるが、

env BUNDLE_GEMFILE=/path/to/Gemfile RUBYOPT=-rbundler/setup ruby -ryour_lib

のように書くと ruby は your_lib.rb を load してから bundler/setup を load する。 ref. https://bugs.ruby-lang.org/issues/5445

digdag の rb オペレータは require: your_lib のように指定したスクリプトを ruby -ryour_lib のように require するので、RUBYOPT よりも先に読み込んでしまい、RUBYOPT=-rbundler/setup を指定しても役に立たない(ことが多い)。


digdag 0.9.35 から ruby パスに加えてオプションを指定できるようになった

_export:

rb:
ruby: ["/path/to/ruby", "-rbundler/setup"]

+task:
rb>: MyWorkflow.method
require: tasks/my_workflow

もしくは

+task:

rb>: MyWorkflow.method
ruby: ["/path/to/ruby", "-rbundler/setup"]
require: tasks/my_workflow

こちらの場合は ruby -rbundler/setup -rtasks/my_workflow のような順番で require するので、bundler が有効になってから、 tasks/my_workflow が require されて tasks ファイル中で gem を使える。


やりたいこと

digdag の project root をワーキングディレクトリにしたまま、別のディレクトリにある Gemfile を指定して ruby 実行したい。

あらかじめ bundle install しておいたdocker イメージを使って rb>: オペレータを使いたい場合、digdag ホストの project root ではなく、コンテナ内の別のパスに Gemfile を置いているはずなので、その場合のシステム要件でもある。


やり方


方法1: BUNDLE_GEMFILE を指定して task ファイルで require 'bundler/setup' する


tasks/workflow.rb

require 'bundler/setup'

require 'your_favorite_gem'

class Workflow
def foo
end
end



sample.dig

+task:

_env:
BUNDLE_GEMFILE: /path/to/Gemfile
rb>: Workflow.foo
require: tasks/workflow

_export: docker: で rb オペレータを動かすのに docker を使っている時も使える。


sample_docker.dig

+task:

_export:
docker:
image: your_image_bundle_installed_inside
_env:
BUNDLE_GEMFILE: /path/to/Gemfile/in/docker
rb>: Workflow.foo
require: tasks/workflow

欠点: 普通に ruby スクリプトを書くと require 'bundler/setup' を手書きすることはまず無いので違和感はある


方法2: BUNDLE_GEMFILE を指定して ruby オプションに -rbundler/setup を指定する

digdag >= 0.9.35 から可能になった。方法1の欠点がなくなる。


tasks/workflow.rb

require 'your_favorite_gem'

class Workflow
def foo
end
end



sample.dig

+task:

_env:
BUNDLE_GEMFILE: /path/to/Gemfile
rb>: Workflow.foo
ruby: ["ruby", "-rbundler/setup"]
require: tasks/workflow

_export: docker: で rb オペレータを動かすのに docker を使っている時も使える。


sample_docker.dig

+task:

_export:
docker:
image: your_image_bundle_installed_inside
rb:
ruby: ["ruby", "-rbundler/setup"]
_env:
BUNDLE_GEMFILE: /path/to/Gemfile/in/docker
rb>: Workflow.foo
require: tasks/workflow


方法3: ラッパースクリプトを用意する

digdag >= 0.9.33 で有効


bundle_exec_ruby

#!/bin/bash

export BUNDLE_GEMFILE=/path/to/Gemfile
exec /path/to/ruby -rbundle/setup $*



tasks/workflow.rb

require 'your_favorite_gem'

class Workflow
def foo
end
end



sample.dig

+task:

rb>: Workflow.foo
require: tasks/workflow
ruby: /path/to/bundle_exec_ruby

_export: docker: で rb オペレータを動かすのに docker を使っている時は docker イメージにスクリプトを入れ込めば使える。


sample_docker.rb

+task:

_export:
docker:
image: your_image_bundle_installed_inside
rb>: Workflow.foo
require: tasks/workflow
ruby: /path/to/bundle_exec_ruby/in/docker

欠点: bash スクリプトを用意するというひと手間が必要。


おわりに

個人的には .dig の外での準備が不要という点で、方法2が良いかとおもっている。