Docker環境でRailsのRakeタスクを作成し、バッチ処理のスケジュールを設定してみた

# バッチ処理とは

リアルタイムでの処理が難しかったり、一気に大量のデータを処理する場合に使用します。

また、毎日、毎週、または特定の時間帯に定期的に実行することも多いです。

具体的には

- データベースのメンテナンス: 定期的なバックアップや不要なデータの削除
- メール送信: 定期的に大量のメールを送信する処理
- レポート生成: 大量のデータを集計してレポートを生成する
- データ移行: 異なるシステム間でデータを移行する

などが挙げられます。

実行方法に関しては色々ありますが、今回はRakeタスクを作成とwheneverを使用して定期的にバッチ処理を実行するまでをやってみました。

### 開発環境
- Rails 7.1.3
- Ruby 3.3.3
- Docker 24.0.7, build afdd53b

### 前提条件
今回は```user.rb```に必要なカラムとメソッド等を用意しています。


```ruby:user.rb
# id :bigint not null, primary key
# age(年齢) :integer
# birthday(誕生日) :date
# name(イニシャル) :string

# 本日誕生日のユーザーを絞り込む
scope :with_birthday_today, lambda {
  today = Time.zone.today
  where("EXTRACT(MONTH FROM birthday) = ? AND EXTRACT(DAY FROM birthday) = ?", today.month, today.day)
}

# 生年月日から年齢を計算してageにセットする
def set_age
  self.age = (Time.zone.today.year - birthday.year) - (Time.zone.today.month < birthday.month || (Time.zone.today.month == birthday.month && Time.zone.today.day < birthday.day) ? 1 : 0)
end
```

### 実装内容
ユーザー(User)の誕生日(birthday)当日に年齢(age)を更新するバッチ処理を作成。
毎日0:00に上記バッチ処理を実行するよう設定する。


# Rakeタスクの作成

下記コマンドを実行することで、```lib/tasks/user.rake```が作成されます。

```
$ docker-compose run web rails g task user
```

デフォルトでは以下のような記載がされています。
```ruby:user.rake
namespace :user do
end
```

生成されたファイルを編集してみます。

```ruby:user.rake
namespace :user do
  desc "今日が誕生日のユーザーの年齢を再計算して保存する"
  task update_age: :environment do
    # 本日誕生日のユーザーを取得
    users = User.with_birthday_today

    # 対象のユーザーが存在しない場合にログに出力
    Rails.logger.info puts "今日が誕生日のユーザーは存在しませんでした。" if users.blank?
    
    users.each do |user|
      # ユーザーの年齢を再計算してセットする
      user.set_age

      # 成功/失敗に関わらずログを出力
      if user.save
        Rails.logger.info puts "#{user.name}の年齢の更新に成功しました"
      else
        Rails.logger.error puts "年齢の更新に失敗しました"
      end
    end
  end
end
```


# Rakeタスクを実行する
以下のコマンドで実行が出来ます。

```
# コマンド rails ファイル.定義したタスク名
$ docker-compose run web bundle exec rake user.update_age
```

# 定期的に実行するように設定する
今回は毎日0:00に実行するように設定していきます。

### 1. wheneverをインストール

[whenever](https://github.com/javan/whenever)というgemをGemfileに追加します。

```ruby:Gemfile
gem 'whenever', require: false
```

Dockerコンテナ内で```bundle install```を実行して、```whenever```をインストールします。

```bash
$ docker-compose run web bundle install
```

### 2. wheneverの設定
```wheneverize .```を実行して、```config/schedule.rb```ファイルを生成します

```
$ docker-compose run web bundle exec wheneverize .

# 実行結果
[add] writing `./config/schedule.rb'
[done] wheneverized!
```

### 3. schedule.rb の編集

ログを保存するファイルを作成しておく(これは好み)
```bash
$ touch log/cron.log
```

```config/schedule.rb```に次のように記述して、毎日0:00にタスク ```user:update_age```を実行するように設定します。


```rubby:schedule.rb
# 環境変数を cron ジョブに渡す(重要！)
ENV.each { |k, v| env(k, v) } 
# ログを保存
set :output, "log/cron.log"

every 1.day, at: '12:00 am' do
  rake 'user:update_age', environment: ENV['RAILS_ENV']
end
```

### 5. cronをインストール

Dockerイメージに```cron```をインストールします。

Dockerfileに以下を追加します。

```Dockerfile
&& apt-get install -y cron nano \
```

その後、イメージを再ビルドします。

``` bash
$ docker-compose build
```


```cron```のインストール確認

```
docker compose run --rm --no-deps web which cron

# 実行結果
Starting periodic command scheduler: cron.
/usr/sbin/cron
```

# 6. cron関連の設定

```entrypoint.sh```スクリプトを作成

``` bash
$ touch entrypoint.sh
```

```entrypoint.sh```を編集

``` ruby:entrypoint.sh
#!/bin/bash
set -e

# cronサービスを起動
service cron start

# Railsサーバーを起動
exec "$@"
```

コンテナ内でcronサービスを起動する設定を追加します。

Dockerfileに以下を追加して、cronを起動するスクリプトを作成します。

``` Dockerfile
# entrypoint.sh スクリプトをコンテナ内の /usr/bin/entrypoint.sh にコピー
COPY entrypoint.sh /usr/bin/entrypoint.sh

# entrypoint.sh に実行権限を与える (実行可能なスクリプトにする)
RUN chmod +x /usr/bin/entrypoint.sh

# コンテナ起動時に実行するコマンドとして entrypoint.sh を指定
CMD ["/entrypoint.sh"]
```

```docker-compose.yml```に追記

```docker-compose.yml
services:
  web:
    # 追加
    entrypoint: ["/usr/bin/entrypoint.sh"]
```

```cron```がバックグラウンドで正しく起動しているか確認

``` bash
$ docker-compose run web service cron status

# 実行結果
Starting periodic command scheduler: cron.
cron is running.
```

cronジョブの更新をする
```
$ docker-compose run web bundle exec whenever --update-crontab

# 実行結果
tarting periodic command scheduler: cron.
[write] crontab file updated
```

イメージを再ビルドします。
```
docker-compose build
```

現在登録されている```cron```ジョブを確認

``` bash
$ docker compose exec web crontab -l

# 実行結果
・
・
・
0 0 * * * /bin/bash -l -c 'cd /current_dir && RAILS_ENV=development bundle exec rake user:update_age --silent >> log/cron.log 2>&1'

# End Whenever generated tasks for: /current_dir/config/schedule.rb at: 2024-12-29 09:01:02 +0000
```

# 最後に

設定時間以降に
``` bash
$ cat log/cron.log
```

を実行して、今回の場合であれば以下のログが残っていれば設定と動作が出来ています！

```
[ユーザー名]の年齢を[ユーザーの年齢]歳に更新しました"
or
今日が誕生日のユーザーは存在しませんでした。
```