renv と Docker の相互運用パターン

概要

  • R のプロジェクトで renv(パッケージ管理システム)と Docker を同時に使う利用法として3種類のパターンを取り上げます。
  • 多くの分析プロジェクトでは renv パッケージキャッシュを外から Docker コンテナにマウントする “外付けパッケージキャッシュパターン” がおすすめです。

背景

データ分析においては、同じ分析コードから同じ結果を得られること、すなわち 再現性 が重要な課題として認識されています。

R を使った分析ではよくパッケージを利用しますが、多くのパッケージは頻繁にバージョンアップされています。そのため、同じ分析コードでも別のマシン上で実行しようとすると、インストールされているパッケージのバージョンが違うために動作が変わってしまうという問題がよく発生します。

この問題に対して、最近は R 実行環境に再現性を持たせるための方法として renvDocker がよく利用されるようになっています。 両者の目的は近いのですがその実現方法は大きく異なり、それぞれの方法に一長一短があります。 また、2つを同時に組み合わせて利用することもできます。

renv と Docker についてまだよく知らない方はまず

などを見ていただくのが良いかと思います。

この記事では renv と Docker の性質を比較した上で、両者を同時に組み合わせて使う運用パターンをいくつか考えてみます。


renv と Docker の比較

基本的な仕組み

renv と Docker はいずれも

  • 再現性 — いつでも・どのマシン上でも同じ R 実行環境を利用できること
  • 独立性 — 同じマシン上でもプロジェクトごとに異なる R 実行環境を利用できること

を実現する意味で共通していますが、それぞれの仕組みは大きく異なっています。

renv の仕組み

  • renv はプロジェクトごとに固有の R ライブラリを作成します。ここで R ライブラリとは、インストールされた R パッケージの集合を意味します。renv ではこのプロジェクト固有の R ライブラリを プライベートライブラリ と呼んでいます。
  • R ライブラリの状態(= インストールされたパッケージとそのバージョン・インストール元リポジトリ・ハッシュ値 のリスト)を1つのファイル renv.lock に記録します。これを ロックファイル といいます。ロックファイルの情報を元にパッケージをインストールし直せば、同じ R ライブラリを再構築できるようになっています。
  • 異なるプロジェクトでは異なる R ライブラリを使用することによって、プロジェクト間で実行環境を分離しています。
  • マシン上にひとつの パッケージキャッシュ を作成し、プロジェクト間で共有しています。これによってパッケージのインストール時間やストレージ使用量を削減しています。

図:renv ロックファイルによる R ライブラリの再構築

図:renv パッケージキャッシュとプライベートライブラリ

Docker の仕組み

  • R 実行環境を Docker コンテナ として作成します。Docker コンテナの状態は Docker イメージ として保存することができます。
  • rocker プロジェクト によって標準的な R 実行環境用の Docker イメージが公開されています。
  • この Docker イメージには R ライブラリのほか OS や R 本体、RStudio Server なども含まれます。
  • Docker イメージは Docker レジストリを経由して他のマシンに容易に移動・複製することができます。同じ Docker イメージからは同じ Docker コンテナを起動することができます。
  • Docker コンテナ同士は互いに分離されます。プロジェクトごとに別の Docker コンテナを起動すれば、それぞれ異なる R 実行環境を利用することができます。
  • Docker イメージはレイヤー構造になっており、同一のレイヤはイメージ間で共有されます。これによってイメージのビルド時間や転送時間、ストレージ使用量を削減しています。

図: Docker の仕組み

仕組みの違い

2つの仕組みの違いを整理すると次のようにまとめることができます

renv Docker
独立性の実現方法 プロジェクトごとに
固有の R ライブラリ を作成する
プロジェクトごとに
固有の Docker コンテナ を作成する
再現性の実現方法 いつ・どのマシンでも
同じ R ライブラリを 再構築する
いつ・どのマシンにも
同じ Docker コンテナを 複製する
効率化の方法 同一マシン内のプロジェクト間で
パッケージキャッシュを共有する
イメージ間でレイヤーを共有する

性質の違い

このように基本的な仕組みが違うことから、renv と Docker には次のような性質の違いが生まれます

renv Docker
対象範囲 管理対象は R パッケージに限る OS や R 本体のバージョン、C/C++ ライブラリ、
その他の設定ファイル等もすべて1つの
Docker イメージにまとめることができる
R 実行環境構築の再現性 同じ renv.lock ファイルから
同じ R ライブラリを再構築できる
同じソースから同じ Docker イメージが
再構築されることを保証する仕組みではない

特に

  • renv では OS や R 本体のバージョン、C/C++ ライブラリ などのシステム環境までは管理できないこと
  • また、Docker 自体にはイメージのビルド時に特定バージョンの R パッケージをインストールするような仕組みは含まれていないこと

に注意が必要です。

組み合わせの方針

このような違いを踏まえて

  • R パッケージは renv で管理し、それ以外の OS や R 本体のバージョン、その他の依存関係、システム設定などは Docker で管理する
  • Docker イメージのビルド時に renv を使用することでビルドの再現性を向上する

のような組み合わせかたが考えられます。

ただし、実際に両者を同時に利用しようとするときに1つ問題となるのが renv のパッケージキャッシュをどうやって Docker で利用するかという点です。 以下ではより具体的な利用パターンを見ていきますが、その中でこのキャッシュの扱い方が鍵となります。


renv と Docker の相互運用パターン

以下 renv と Docker の相互運用について3種類の利用パターンを見ていきます。今回その3つをそれぞれ

と名付けてみました。

上の2つは renv パッケージの公式ドキュメント “Using renv with Docker” で紹介されている方法です。

結論から言うと、 多くの分析プロジェクトでは外付けパッケージキャッシュパターンがおすすめ です。


組み込みライブラリパターン

[概要]

1つのプロジェクトに対して1つの Docker イメージをビルドし、それをプロジェクト参加者間で共有する方法です。 Docker イメージをビルドする際に renv を利用して指定されたバージョンの R パッケージをインストールします。

[利用シーン]

  • 分析結果の厳密な検証が必要なとき
  • 勉強会の演習環境として利用するとき

[良し悪し]

  • 長所
    • システム環境とインストール済みパッケージをまとめて1つの Docker イメージに「固めて」しまうので、強力な再現性を持ちます。
    • Docker イメージを pull してコンテナを起動するだけなので、シンプルに利用することができます。
  • 短所
    • プロジェクトごとに Docker イメージのビルドが必要になります。Docker イメージのビルド中は renv のパッケージキャッシュが利用できないため、パッケージをインストールするのに時間が掛かってしまいます。
    • 利用するパッケージを変更した場合、イメージの再ビルドが必要になります。
    • パッケージインストール済みのイメージをまるごと push/pull するのに時間が掛かる可能性があります。
    • プロジェクト間でパッケージを共有しないので、プロジェクトが増えるとストレージ使用量が多くなる可能性があります。

[構成]

1つの Git リポジトリと1つの Docker レジストリを用意し、以下のような内容とします:

  • Git リポジトリ:
    • Docker イメージビルド用のソースファイル:
      • Dockerfilerenv.lock などイメージのビルドに必要なファイルを適当なフォルダにまとめておく
      • Dockerfile に renv 自体のインストールと renv::restore() によるパッケージのインストールを記述
    • 分析用ファイル:
      • RStudio Server 上で実行するための R スクリプトや分析に使用するデータファイルなど
  • Docker レジストリ:
    • ビルドした Docker イメージをプロジェクト参加者に配布するために用意する。
    • 公開でもよければ Docker Hub が無料で使用可能。非公開とするには Docker Hub 有料版や Google Container Registry, Amazon ECR などを利用する。
  • Docker イメージ
    • Docker レジストリに保存
    • ベースイメージは rocker/rstudiorocker/tidyverse など
    • 日本語フォント、画像処理ライブラリなどインストール済み
    • 加えて R パッケージもインストール済み
  • Docker コンテナの起動時設定
    • Docker ホストにある分析用ファイルをコンテナ内にマウントする。

図: 組み込みライブラリパターンの Docker イメージ と Docker コンテナ

[役割]

  • プロジェクト所有者
  • プロジェクト参加者

[流れ]

環境構築 〜 分析作業を次のような流れで行います:

  • プロジェクト所有者は
    1. Git リポジトリを作成してコードをコミットする。
    2. Docker イメージをビルドする。このとき Dockerfile の内容に従い renv でパッケージがインストールされる。
    3. Docker イメージを Docker レジストリに push する。
    4. Git リポジトリと Docker イメージをプロジェクト参加者に共有する。
  • プロジェクト参加者は各自のローカル環境で
    1. Docker Desktop をインストールしておく
    2. Git リポジトリをクローンする
    3. Docker レジストリから Docker イメージを pull する
    4. pull した Docker イメージから Docker コンテナを起動する。このとき、分析用ファイルをコンテナ内にマウントする。
    5. コンテナで起動した RStudio Server にブラウザから接続する
    6. RStudio Server 上で分析コードの実行・編集などを行う

[バリエーション]

Docker レジストリの準備や Docker イメージの push/pull が面倒なら、Git でソースコードのみを共有して各自のマシン上で Docker イメージをビルドすることも可能です。ただし、その場合はビルドされたイメージが他のマシンでビルドされたものと確実に同一の状態になるとは限らない点に注意が必要です。


外付けパッケージキャッシュパターン

[概要]

コンテナ起動時に renv のパッケージキャッシュを Docker ホストからマウントして利用する方法です。 プライベートライブラリへのパッケージインストールは Docker イメージのビルド時ではなく Docker コンテナの起動後に行います。 多くの分析プロジェクトではこれがおすすめです。

[利用シーン]

  • 業務上の分析プロジェクト全般

[良し悪し]

  • 利点
    • パッケージキャッシュが利用できるため、R パッケージのインストールを高速化できます。
    • Docker イメージの共有により R パッケージ以外のシステム環境も統一することが可能です。
    • プロジェクトで利用する R パッケージを変更するたびに新しい Docker イメージをビルドする必要がありません。
  • 欠点
    • Docker ホスト上のパッケージキャッシュについて、マシンごとに異なる初期設定が必要になります。
    • コンテナ起動時の設定が少し複雑になります。

[サンプルコード]

GitHub リポジトリ terashim/renv-docker-example-external-package-cache でサンプルコードを公開しています。

[構成]

1つの Git リポジトリと1つの Docker レジストリを用意し、以下のような内容とします:

  • Git リポジトリ:
    • Docker イメージビルド用ソース:
      • Dockerfile などイメージのビルドに必要なファイル
    • 分析用ファイル:
      • RStudio Server 上で実行するための R スクリプトや分析に使用するデータファイルなど
      • renv.lock ファイルは分析用ファイルの1つとして扱い、他のファイルと同様にコンテナ起動時にマウントする。
  • Docker レジストリ:
    • ビルドした Docker イメージをプロジェクト参加者に配布するために用意する。
    • 公開でもよければ Docker Hub が無料で使用可能。非公開とするには Docker Hub 有料版や Google Container Registry, Amazon ECR などを利用する。
  • Docker イメージ
    • Docker レジストリに保存
    • ベースイメージは rocker/rstudiorocker/tidyverse など
    • 日本語フォント、画像処理ライブラリなど R パッケージ以外がインストール済み
    • R パッケージはインストールされていない

また、各マシン環境で renv のパッケージキャッシュを準備します。といってもこれはパッケージの保存先となるただのフォルダです。 例えば macOS なら /Users/ユーザー名/Library/Application Support/renv が標準的なパッケージキャッシュのパスです。 適当にパスを決めて、フォルダが存在しなければ作っておきます。

コンテナ起動時の設定は次のようにします:

  • 環境変数
    • 環境変数 RENV_PATHS_CACHE の値として(Docker コンテナ側の)パッケージキャッシュのパス(例: /home/rstudio/.local/cache/renv)を設定する。
  • ボリュームのマウント
    • Docker ホスト側のパッケージキャッシュをコンテナ内のパッケージキャッシュ(例: /home/rstudio/.local/cache/renv)にマウントする。
    • 分析用ファイルをコンテナの適当な場所(/home/rsutdio/プロジェクト名 など)にマウントする。

図:外付けパッケージキャッシュパターン

このようにコンテナ起動時の設定がやや複雑になるので、Docker Compose を利用して docker-compose.yml ファイルに設定を書き込んでおくのがおすすめです。

[役割]

  • プロジェクト所有者
  • プロジェクト参加者

[流れ]

環境構築 〜 分析作業を次のような流れで行います:

  • プロジェクト所有者は
    1. Gitリポジトリを作成する
    2. Docker レジストリを用意する
    3. ソースファイルを Git リポジトリにコミットする
    4. Docker イメージをビルドして Docker レジストリに push する
    5. Git リポジトリと Docker イメージをプロジェクト参加者に共有する
  • プロジェクト参加者は各自のローカル環境で
    1. Docker Desktop をインストールしておく
    2. Docker ホスト側のパッケージキャッシュを作成しておく
    3. Git リポジトリをクローンする
    4. Docker レジストリからイメージを pull する
    5. Dockerイメージからコンテナを起動する。その際、上述のようにボリュームマウントや環境変数の設定を行う。
    6. コンテナで起動した RStudio Server にブラウザから接続する
    7. RStudio Server 上で renv::restore() を実行して、そのプロジェクトのプライベートライブラリにパッケージをインストールする
    8. RStudio Server 上で分析コードの実行・編集などを行う

[バリエーション]

  • rocker 系イメージをそのまま使える場合は、Docker イメージのビルドや push/pull による共有は不要です。OS と R 本体のバージョンを揃えるだけならこれでも十分 と言えます。
  • 厳密な再現性を求めなければ、Docker イメージは各自のローカル環境でビルドしても良いでしょう。
  • マシン環境が適合していれば Docker を使わずにデスクトップ版の RStudio で renv を使ってコードを動かすことも可能です。

コンテナ化アプリケーション開発パターン

このパターンは RStudio 上で分析を行うのではなく、R を使ってアプリケーション開発を行うときの利用法です。 このようなシーンでは「開発中はボリュームのマウントを利用して高速にファイルの変更と動作検証を行いたいが、本番環境ではすべてのファイル込みでビルドされたコンテナをデプロイしたい」という状況が考えられます。

そこで、前の2つのパターンを用途に応じて切り替えるような方法が有効になります。

[利用シーン]

  • バッチ処理用の R プログラム開発
  • Shiny によるウェブアプリケーション開発
  • plumber による API 開発

[良し悪し]

  • 長所
    • 開発環境では頻繁なソースコード編集や R パッケージのインストール・バージョン変更を気軽に行うことができます。
    • 本番環境ではアプリケーションのソースと R ライブラリ、およびその他のシステム環境を含む実行環境全体を1つのコンテナにまとめてデプロイできます。
  • 短所
    • セットアップが大掛かりになります。通常の分析プロジェクトでは不要です。

[構成]

次のような構成とします:

  • Git リポジトリ
    • アプリケーションや Docker イメージのソースコードを保存
  • Docker レジストリ
    • ビルドされた Docker イメージを保存
  • Docker コンテナ
    • ローカル開発環境用の Docker イメージと本番環境用の Docker イメージを別にする
    • ローカル開発環境用のコンテナ
      • イメージのビルド時
        • rocker/rstudio など RStudio Server の入ったイメージをベースとする
        • アプリケーションのソースコードを含めない
        • パッケージはインストールしない
      • コンテナ起動時
        • アプリケーションのソースコードを Docker ホストからマウントする
        • renv パッケージキャッシュを Docker ホストからマウントする
        • 起動後に renv でパッケージをインストールする
    • 本番環境用のコンテナ
      • イメージのビルド時
        • rocker/shiny など RStudio の入っていないもの をベースイメージとする
        • アプリケーションのソースコードをイメージ内に COPY する
        • renv を使ってイメージ内にパッケージをインストールする
      • コンテナ起動時
        • ボリュームのマウントは不要
  • コンテナのデプロイ先となる本番環境・テスト環境等を用意
  • 必要に応じて CI/CD ツール等と連携

さらに、開発用と本番用の2つの Docker イメージを扱うのに マルチステージビルド も活用すると良いかもしれません。

[役割]

  • アプリケーション開発者

[流れ]

アプリケーション開発者は次のような流れで開発作業を行います:

  1. ローカル開発環境用 Docker イメージからコンテナを起動して、その RStudio Server 上でコードの編集やテストを行う。
  2. 開発中にパッケージをインストール・アップデートなどした場合は renv::snapshot() でロックファイル renv.lock を更新する。
  3. リリース可能な状態になったら本番環境用イメージをビルドする。
  4. ビルドされた本番環境用イメージをテストして本番環境にデプロイする。

図:コンテナ化アプリケーション開発パターン

[バリエーション]

利用するクラウドプラットフォームや CI/CD ツール、サービス基盤等の組み合わせは無数に考えられます。 サービスの規模や要件に合わせて選定することが肝心です。


雑感

R による分析プロジェクトにおいて、再現性の問題は多くの場合パッケージのバージョンに起因するかと思います。パッケージのバージョン固定は renv だけで可能なので、これから再現性の課題に取り組み始めたい方はまずデスクトップ版 RStudio と renv の組み合わせを試してみるのが良いのではないかと思いました。逆に、Docker は単にライブラリの状態を共有するための手段としてはやや大仕掛け過ぎると感じるようになりました。

外付けパッケージキャッシュパターン は後から Docker を導入しやすいのもメリットです。 最初は renv でパッケージ管理のみ行い、後から Docker による OS や R 本体の管理を加えた形へと自然に移行することができます。

この記事は概念的な内容の説明となりましたが、現在具体例として3つのパターンそれぞれのサンプルコードを作成中です。公開の際はこのブログや Twitter でお知らせする予定です。

(8/31 追記) 外付けパッケージキャッシュパターンの サンプルコード を GitHub で公開しました。


参考

renv パッケージの公式ドキュメントと Rocker プロジェクトの公式サイトはそれぞれ

にあります。 本記事の内容は主に renv パッケージ公式ドキュメント内の Docker に関する解説

に基づいています。

renv の日本語解説は

が、R ユーザ向けの Docker 入門としては

がおすすめです。