2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R言語で実装されたシステムのCI構築

Posted at

システム化するということ

R言語は、データ分析や統計モデリングにおいて非常に強力なツールですが、分析を単に行う場合と、その実装をシステム化する際には、求められるものが大きく異なります。単なる分析では、分析コードが実行できれば十分ですが、システム化する場合には、コードの品質、再現性、保守性の確保が不可欠となります。

R言語での実装をシステム化する際に、エンジニアが特に意識すべきものとして

  • パッケージ管理
  • lint & format
  • test
    の3つの側面について解説していきます。

サンプルリポジトリ

本記事で記載している実装を実際にリポジトリに書いているので、こちらを参考にしながら読んでいただくと幸いです。
https://github.com/Yashikab/sample_r_system

ディレクトリ構成

今回のサンプルリポジトリでは下記のディレクトリ構成になっています。

.
├── Dockerfile
├── Makefile
├── README.md
├── hack
│   ├── check_status.R
│   ├── fmt.R
│   ├── install.R
│   ├── lint.R
│   ├── synchronize.R
│   └── test.R
├── renv
│   ├── activate.R
│   ├── library
│   ├── settings.json
│   └── staging
├── renv.lock
└── src
    ├── fibonacci.R
    ├── sample.R
    └── tests
  • hackディレクトリ: 以下で紹介するCIで実行されるスクリプトを格納します
  • src ディレクトリ: コードの実装とテストはすべてここで行います。hackスクリプトのチェック対象ディレクトリです
  • renvディレクトリ: パッケージ管理ツールのrenvを初期化すると作られるディレクトリです
  • renv.lockファイル: renvのステータスを管理するlockファイルです

リポジトリをクローンしたときのまずやること

make installを実行することで renv.lockのステータスのライブラリバージョンのインストールなどができ、リポジトリのライブラリ状態でローカルの環境構築ができます。
ただ、R自体のバージョンは合わせておく必要があります。(ここも管理できるようにしたいですが僕の力ではわからなかった。。)

パッケージ管理

パッケージ管理の必要性

R言語のエコシステムは非常に活発であり、数多くのパッケージが開発されています。これらのパッケージを活用することで、複雑なデータ分析を効率的に行うことができます。しかし、パッケージのバージョンが異なることで、コードの動作が不安定になったり、再現性が失われたりする問題が発生する可能性があります。

また、チームで共同で開発を行う場合、メンバー間で使用するパッケージのバージョンが異なることで、環境の差異が生じ、エラーの原因となることがあります。

Rでのパッケージ管理

Rのパッケージ管理ツールはいくつかありますが、今回は renvを利用したいと思います。
renv は、Rのパッケージ管理ツールです。プロジェクトごとにパッケージのバージョンを固定し、再現性のある開発環境を構築することができます。

  • snapshot: プロジェクトの現在のライブラリ状態を「lockファイル」に保存します。このファイルは、プロジェクトの依存関係を記録したもので、いわば「レシピ」のようなものです。
  • restore: lockファイルの内容に基づいて、プロジェクトのライブラリ環境を再現します。これにより、異なるマシンや、時間の経過によっても、同じ開発環境を構築することができます。

renvの公式ページはこちらを参考にしてください。
また下記のqiita記事が大変わかりやすく参考にさせていただきました。ありがとうございますmm
(参考: https://qiita.com/okiyuki99/items/688a00ca9a58e42e3bfa)

同期チェックスクリプト

システム化する上で、ライブラリが変更された際に同期されている必要がありますが、これを忘れると意味がなくなります。そこで同期がとれているか確認するスクリプトを用意してCI実行時に同期されていない場合は失敗するようにします。

hack/check.R
# 同期されているかチェックするスクリプト
library("renv")
# get status
status <- renv::status()

# check if there are any changes
if (status$synchronize) {
    message("There are no changes in the project library.")
    quit(status = 0)
} else {
    # If there are some change, report it and exit with error
    message("There are changes in the project library.")
    stop("Please run 'renv::snapshot()' to save the changes.")
}

同期用のスクリプト

snapshotを実行して同期をとるスクリプトも用意します。

hack/synchronize.R
# snapshotをとるスクリプト
library("renv")

# install on uninstalled packages
renv::install()

# take snapshot
renv::snapshot()

installスクリプト

リポジトリをクローンしたときのまずやることでも書きましたが、ローカルの環境構築をrenvを使って構築するためのスクリプトです。
中身はただ renv::restore()しているだけです。

hack/install.R
library("renv")
renv::restore()

Lint & Format

どの言語にも言えることですが、Lint & Formatは、コードの品質を保ち、開発効率を向上させるために非常に重要な役割を果たします。

Lintの実装

lintr を利用してLintを記述します。

hack/lint.R
library("lintr")

result <- lintr::lint_dir("src")

if (length(result) > 0 ) {
    print(result)
    stop("Linting failed")
} else {
    print("Linting passed")
}

Formatの実装

styler を使用してformatterを実装します。

hack/format.R
library("styler")

# format on src directory
styler::style_dir("src")

Test

単体テストを実装することで、コード品質を担保しましょう。
テスト自体は src/tests フォルダに書かれることを想定して、これらをCIで実行されるようなスクリプトを用意しておきます。

hack/test.R
library("testthat")

test_dir("src/tests")

makeファイルの用意

今回はあまり必要性ないですが、他言語の管理もやるときなどはmakefileを使ってまとめたほうがCI実装時に役立ちます。

Makefile
fmt:
	Rscript hack/fmt.R

lint:
	Rscript hack/lint.R

test:
	Rscript hack/test.R

check_R:
	Rscript hack/check_status.R

sync_R:
	Rscript hack/synchronize.R

install:
	Rscript hack/install.R

githubへのpush前に make sync_R を実行して常にライブラリの同期をとることを開発ルールに定めましょう。

CI実装

同期チェックをCIに組み込むことで同期を忘れたままシステムがデプロイされることを防ぎます。
今回は、github actionsをCI環境として利用し、組み込み方法を2つ紹介します。

github actionsのrunnerにR環境を構築してCI実行

actionsのライブラリを使用してR環境をrunnerに入れます。
これは github actionsを利用する場合しか参考にならないですが、こっちのほうが実行速度は早いのと手順は簡単です。
以下のようにworkflowを用意します。

name: Renv use directory flow
on:
  push:


jobs:
  ci-check:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: '4.4.1'

      - name: Install renv
        uses: r-lib/actions/setup-renv@v2

      - name: Check status
        run: |
          make check_R

dockerコンテナにCI環境を入れる

dockerイメージにCI環境をつくって、github actionsでコンテナを実行するパターンです。
github actions以外のCIツールにも適用しやすいですが、dockerビルドに時間がかかり、少し遅いです。

まず Dockerfileをmultistageで用意します。
renvパッケージを引き継ぐには、renvフォルダをコピーし、cacheのパスを環境変数で指定します。

# r-base
FROM r-base:4.4.1 AS base
RUN apt-get update \
    && apt-get install -y --no-install-recommends libxml2-dev \
    && rm -rf /var/lib/apt/lists/*

RUN R -e "install.packages('renv')"

RUN mkdir -p /basedir
WORKDIR /basedir
RUN mkdir -p renv
COPY renv.lock renv.lock
COPY .Rprofile .Rprofile
COPY renv/activate.R renv/activate.R
COPY renv/settings.json renv/settings.json

RUN mkdir renv/.cache
ENV RENV_PATHS_CACHE=renv/.cache
RUN R -e "renv::restore()"


FROM base AS ci

ENV GITHUB_WOKRDIR=/__w/sample_r_system/sample_r_system
RUN mkdir -p /${GITHUB_WOKRDIR}

ENV WORKSPACE=ci
RUN mkdir -p /${WORKSPACE}
WORKDIR /${WORKSPACE}
COPY --from=base /basedir .

COPY hack /${WORKSPACE}/hack
COPY Makefile /${WORKSPACE}/Makefile

ENTRYPOINT [ "make" ]

github actionsでは dockerのtarget ciを dockerhubにビルドしてこれを使って make check_R を実行します。

name: R sample flow
on:
  push:

jobs:
  ci-image-build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: yashikab/r-sample-ci:latest
          target: ci
          cache-from: type=registry,ref=yashikab/r-sample-ci:buildcache
          cache-to: type=registry,ref=yashikab/r-sample-ci:buildcache,mode=max

  ci-check:
    needs: ci-image-build
    runs-on: ubuntu-latest
    container:
      image: yashikab/r-sample-ci:latest
      credentials:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    defaults:
      run:
        working-directory: /ci
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: copy sources
        run: cp -r ${GITHUB_WORKSPACE}/src /ci/src
      - name: Check R status
        run: make check_R

開発風景

パッケージ同期忘れ

githubでPRを作って何かを開発したとします。
ためしに、sample2.Rstringrライブラリを読み込んだ場合を考えます。
このとき分析者のローカルにすでにstringrが入っていれば、動作確認では動きます。
そのまま同期を忘れてrenvの管理対象ではないまま githubにコードpushしたとします。
その場合は、check_RによってCIが失敗し、同期されていないことに気づくことができます。

加えたRスクリプト

sample2.R
library("stringr")

x <- "Hello World"

print(stringr::str_length(x))
$ R -e 'install("stringr")'
$ Rscript src/sample2.R

# ----- 出力結果 ----
# - The project is out-of-sync -- use `renv::status()` for details.
# [1] 11

このまま make sync_R せずにコードpushするとCIが落ちる。
https://github.com/Yashikab/sample_r_system/actions/runs/10362721271/job/28685177409

ローカルで make sync_Rを実行するとrenv.lockが更新されて再度pushするとCIが通る。
https://github.com/Yashikab/sample_r_system/actions/runs/10362775516

$ make sync_R

# ---- 出力結果 ----
# Rscript hack/synchronize.R
# - The project is out-of-sync -- use `renv::status()` for details.
# 
# 次のパッケージを付け加えます: ‘renv’
# 
# 以下のオブジェクトは ‘package:stats’ からマスクされています:
#
#    embed, update
#
# 以下のオブジェクトは ‘package:utils’ からマスクされています:
# 
#     history, upgrade
#
# 以下のオブジェクトは ‘package:base’ からマスクされています:
#
#    autoload, load, remove, use
#
# - There are no packages to install.
# The following package(s) will be updated in the lockfile:

# # CRAN -----------------------------------------------------------------------
# - stringi   [* -> 1.8.4]
# - stringr   [* -> 1.5.1]

# - Lockfile written to "~/github/sample_r_system/renv.lock".

参考PR: https://github.com/Yashikab/sample_r_system/pull/1

lintエラー

Rではコードをコメントアウトで残したままにするとコード規約違反になるようです。
ためしに、コードの一部をコメントアウトしてgithubにpushします。
その場合、LintチェックでCIが失敗します。
src/sample.Rでコードをコメントアウトで追加します。

sample.R
print("Hello World")
# print("Hey!")

この状態でpushするとCIが失敗します。
https://github.com/Yashikab/sample_r_system/actions/runs/10362841396/job/28685491998

参考PR: https://github.com/Yashikab/sample_r_system/pull/2

Test

testに関しても同様にCIで管理されていますが、ちょっと書くのが面倒になったので割愛します笑

参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?