🏃

【Rails7・Docker・Sidekiq】Sidekiq導入とメール自動送信のバックグラウンド処理の実装

に公開

自己紹介

はじめまして、はると申します。
駆け出しエンジニアとして働き始めて約1ヶ月が経過しました🐣

概要

RailsアプリにSidekiq(redis)を導入し、メールの自動送信の機能を追加していきます。
この記事のために、メールを10分後に送信するだけのサンプルアプリを用意しました。
完成形のコードはこちらです。

https://github.com/lemonade-37/sidekiq_sample

環境

  • Mac OS(Apple Silicon)
  • Docker
  • Ruby 3.2.3
  • Rails 7.1.3
  • Importmap
  • Tailwindcss、daisyUI
  • PostgreSQL

バックグラウンド処理とは?

Railsアプリケーションでは、ユーザーが特定の動作を行った際に「投稿する」「画面を表示させる」などのアクションを実行します。
バックグラウンド処理とは例えば、「毎日特定の時間にメールを送信する」などの処理です。
ユーザーの行う操作とは独立して、サーバー側で非同期に実行されるタスクのことを言います。

バックグラウンド処理を実装する方法について

バックグラウンド処理を実装する方法はさまざまな手段があります。
調べたもののみですが、それぞれの特徴についてまとめます✏️

cron

Linuxに標準で備わっているプログラムの一種で、設定すると処理を自動で定期的に実行することができます。1)
Railsアプリケーション内だけでなく、決まった時間にアプリ自体を立ち上げるなど、システムレベルでの実行も可能です。

whenever

cronジョブをRubyで扱いやすいように構文を提供してくれているRubyのgemです。
gem whenever

Sidekiq

バックグラウンド処理を行うためのRubyのgemです。
同時に複数のバックグラウンドジョブを実行するなどの実装ができます。
定期的なジョブをスケジュールする拡張機能を使用するための、sidekiq-cronや、sidekiq-schedulerなどの追加gemがあります。
gem sidekiq

Redis

多目的に利用されるインメモリデータストアです。
インメモリデータストアは、すべてのデータをメモリ上に展開し、データの読み込みや追加、変更、削除をすべてメモリ上で完結させることで、従来型の数百倍から数万倍も高速に処理を実行することができるデータベースです。2)
今回はセッション管理として利用しますが、他にも様々な用途で使用されます。

ActiveJob

Rails4.2からrailsのデフォルトで導入された、これまでSidekiqなどのGemを使って行っていたバックグラウンドジョブ系を扱える機能です。
ActiveJobの基礎

使用技術の選び方について

バックグラウンドでどんな処理を実行したいかという目的によって、上記の技術を必要に応じて組み合わせて使用します。

今回は、アプリサーバーは自動で落ちずに常に起動している状態で、決まったタイミングでメールを自動送信するという機能のみなので、
ActiveJob+Sidekiq(Redis) を使用することにしました。
  
ActiveJobを介してSidekiqを使うか、直接Sidekiqを使用するかに関しては、調べてみたところさまざまな価値観があるようでした。

私は下記の記事を参考に、この組み合わせにしました。
(他にも参考にした記事は参考記事参照)

https://qiita.com/seiyatakahashi/items/cb9ae73e5ba3020f4a89#activejobを使用せずsidekiqを利用する

実装開始!🕰

それではここから、メールを送信するボタンを押すとエンキューされ、10分後に自動的にメールが送信される機能を実装していきます。
  
基本的なメール送信機能はすでに実装されている状態からスタートします。
今回は、gem 'letter_opener_web'を使用し、ローカルのみでメール送信の動作を行えるようにしています。
この時点でのコードについて確認したい方は以下を参照ください。

https://github.com/lemonade-37/sidekiq_sample/tree/3af4a9a095d7c4f2f46c616ffb8ec366a40efc43

Sidekiq・Redisの導入

  1. docker-compose.ymlにredisを追加します。

    docker-compose.yml
    redis:
      image: "redis:7.0-alpine"
      volumes:
        - redis_volume:/data
      command: redis-server --appendonly yes
      ports:
        - "6379:6379"
    

    volumes:にもredis_volume:を追加します。

    docker-compose.yml
    volumes:
      postgres_volume:
      redis_volume:
    
    • ここでDokerHubのイメージを使用して、Redisのコンテナを作成します。

    • Dockerのボリュームについての説明は今回は割愛します。(公式ドキュメントを参照ください)

    • ボリュームを設定し、command: redis-server --appendonly yesと記載しAOF Redis を使用することで、
      10分後送信予定のメールのキューがある状態でDockerが停止してしまっても、
      実行予定で溜まっていたキューが削除されずに、
      Dockerを再度立ち上げるとキューの続きから実行することができます。

  2. docker-compose.ymlにsidekiqを追加します。

    docker-compose.yml
    sidekiq:
      build: .
      command: bundle exec sidekiq
      volumes:
        - .:/app
      depends_on:
        - db
        - redis
    
    • depends_on:でdbコンテナとredisコンテナがSidekiqサービスより先に起動するように指定します。
  3. docker compose buildコマンドで再ビルドします。

  4. Gemfileに下記を記載し、docker compose upしてから、docker compose exec web bundle install実行します。

    Gemfile
    # Background Job
    gem 'sidekiq'
    gem 'sidekiq-scheduler'
    

  5. config/application.rbに下記を追加します。

    application.rb
    class Application < Rails::Application
      # 〜〜省略〜〜
      config.active_job.queue_adapter = :sidekiq
      # 〜〜省略〜〜
    end
    

  6. config/initializers/sidekiq.rbを作成し、下記のように記載します。

    sidekiq.rb
    Sidekiq.configure_server do |config|
      config.redis = { url: 'redis://redis:6379' }
    end
    
    Sidekiq.configure_client do |config|
      config.redis = { url: 'redis://redis:6379' }
    end
    

  7. config/sidekiq.ymlを作成し、下記のように記載します。

    sidekiq.yml
    :concurrency: 3
    :queues:
      - default
    
    • :concurrency:は同時並行で実施できるジョブの数の上限を指定しているので、任意の数値でOKです。
  8. docker compose exec web bundle exec sidekiqを実行して、下記のように起動されれば導入完了です🦶

                   m,
                   `$b
              .ss,  $$:         .,d$
              `$$P,d$P'    .,md$P"'
               ,$$$$$b/md$$$P^'
             .d$$$$$$/$$$P'
             $$^' `"/$$$'       ____  _     _      _    _
             $:    ',$$:       / ___|(_) __| | ___| | _(_) __ _
             `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                    $$:         ___) | | (_| |  __/   <| | (_| |
                    $$         |____/|_|\__,_|\___|_|\_\_|\__, |
                  .d$$                                       |_|
    
    
    〜〜省略〜〜
    
    INFO: Starting processing, hit Ctrl-C to stop
    

Sidekiqダッシュボードの設定

  1. config/routes.rbに下記を追加します。
    routes.rb
    require 'sidekiq/web'
    
    Rails.application.routes.draw do
      mount Sidekiq::Web, at: '/sidekiq'
    end
    
  2. Dockerを立ち上げ直し、http://localhost:3000/sidekiq/ にアクセスして、下記の画面が表示されればダッシュボードの設定が完了です。
  3. ※任意 ダッシュボードには一般ユーザーはアクセスできないようにBasic認証をつけるのが望ましいです。
    routes.rb
    require 'sidekiq/web'
    
    Rails.application.routes.draw do
      Sidekiq::Web.use(Rack::Auth::Basic) do |user_id, password|
        [user_id, password] == [ENV['SIDEKIQ_BASIC_ID'], ENV['SIDEKIQ_BASIC_PASSWORD']]
      end
      mount Sidekiq::Web, at: '/sidekiq'
    end
    
    .env
    SIDEKIQ_BASIC_ID=hogehoge
    SIDEKIQ_BASIC_PASSWORD=fugafuga
    
    Basic認証が追加できると以下のように、パスワードを入れないとアクセスできなくなります。

10分後に自動送信されるメールのActiveJob作成

ここからは、メールの定期的な自動送信機能のジョブを作成します。

  1. ActiveJobのファイルを作成します。
    docker compose exec web rails g job SendEmailを実行し、app/jobs/send_email_job.rbを作成します。
    ジョブの中でMail送信を実行するようにします。
    send_email_job.rb
    class SendEmailJob < ApplicationJob
      queue_as :default
    
      def perform(email)
        SampleEmailMailer.sample_email(email).deliver_now
      end
    end
    
  2. コントローラーでJobを呼び出します。
    現在時刻から10分後に実行するキューを追加する形とします。
    controllers/static_pages_controller.rb
    class StaticPagesController < ApplicationController
      def new ; end
    
      def create
        email = params[:email]
    +   SendEmailJob.set(wait_until: Time.zone.now + 10.minutes).perform_later(email) #10分後に実行するジョブをキューに追加
        redirect_to new_static_page_path, notice: 'メールを送信のキューを追加しました'
      end
    end
    
  3. Railsコンソールを立ち上げて、ジョブ自体が実行できるか確認してみます。
    ターミナル
    % docker compose exec web rails c
    Loading development environment (Rails 7.1.3.2)
    irb(main):001> SendEmailJob.perform_now("a@example.com")
    
  4. ジョブが実行され、メールが届いていれば、ジョブ自体の実行はできています。

10分後に自動送信されるメールの動作確認

では、実際アプリ画面でメールを送信し、10分後に実行されるか確認します。

  1. まずDockerを立ち上げた後忘れずに、Sidekiqを立ち上げます。
    (これを忘れているとキューが実行されないので注意⚠️)

    ターミナル
    docker compose exec web bundle exec sidekiq
    

  2. アプリからメールを送信してみます。

  3. Sidekiqのダッシュボードにアクセスし、予定タブに予定されたジョブが追加されていることを確認します。

  4. 10分経過後にメールが自動送信されていることを確認できたら、実装完了です!!🎉

指定した日時に自動送信されるように設定

10分後に送信する以外にも、「毎月の月初にメールを送信する」などの設定をすることもできます。

  1. config/sidekiq.ymlに下記の:scheduler:を追加します。
    sidekiq.yml
    :concurrency: 3
    :queues:
      - default
    :scheduler:
      :schedule:
        send_email_job:
          cron: "0 12 1 * *"
          class: SendEmailJob
          queue: default
    
  2. sidekiqを立ち上げ直した際に、ターミナルに下記が表示されていれば、指定した日時にジョブを実行するスケジュールが設定されています🎉
    ターミナル
    2024-03-31T12:46:26.925Z pid=67 tid=2vv INFO: Scheduling send_email_job {"cron"=>"0 12 1 * *", "class"=>"SendEmailJob", "queue"=>"default"}
    

さいごに

業務の中でメールの自動送信の機能を作成する機会があり、Sidekiqを初めて使用しましたが、
これまで理解できていなかった部分だったため、大変勉強になりました!🕰️
まだまだ理解しきれていない部分も多いため、引き続き勉強していきます🙇

参考・引用記事

引用記事

1)cronとは。crontabで定期実行を自動化する方法を解説
2)e-Words インメモリデータベース

参考記事

Discussion