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

EDIT: 0.9.35 で[ruby オプションを指定できるようになった](https://github.com/treasure-data/digdag/pull/1022)ので追記

## 前提知識

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

読み飛ばし可能。

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

```yaml
_export:
  rb:
    ruby: /path/to/ruby
```

もしくは

```yaml
+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 パスに加えてオプションを指定できるようになった

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

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

もしくは

```yaml
+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' する

```rb:tasks/workflow.rb
require 'bundler/setup'
require 'your_favorite_gem'

class Workflow
  def foo
  end
end
```

```yaml:sample.dig
+task:
  _env:
    BUNDLE_GEMFILE: /path/to/Gemfile
  rb>: Workflow.foo
  require: tasks/workflow
```

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

```yaml: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の欠点がない方法。

```rb:tasks/workflow.rb
require 'your_favorite_gem'

class Workflow
  def foo
  end
end
```

```yaml:sample.dig
+task:
  _env:
    BUNDLE_GEMFILE: /path/to/Gemfile
  rb>: Workflow.foo
  ruby: ["ruby", "-rbundler/setup"]
  require: tasks/workflow
```

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

```yaml: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 で有効

```sh:bundle_exec_ruby
#!/bin/bash

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

```rb:tasks/workflow.rb
require 'your_favorite_gem'

class Workflow
  def foo
  end
end
```

```yaml:sample.dig
+task:
  rb>: Workflow.foo
  require: tasks/workflow
  ruby: /path/to/bundle_exec_ruby
```

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

```yaml: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 の外での準備が不要という点で、方法２が良いかとおもっている。