2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

sidekicq使って定期的なメール配信(rails/Docker/Heroku/circleCI)

Posted at

やりたいこと

自作の「家計簿アプリ」の中で、
定期的(今回は毎週月曜の午前9時)に「支出のまとめ」をメールで配信する。

※完成イメージはgithubのrepositoryを参照してください。

環境

ruby 2.7.4
rails 6.1.4
アプリケーションは、Docker,circleCIを使って、Herokuにデプロイ済み。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./src:/app
      - gem_data:/usr/local/bundle
    environment:
      WEBPACKER_DEV_SERVER_HOST: webpacker
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
Dockerfile
FROM ruby:2.7.4

ENV RAILS_ENV=production

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update -qq \
    && apt-get install -y nodejs yarn build-essential default-mysql-client \
    && apt-get install -y cron
WORKDIR /app
COPY ./src /app
RUN bundle config --local set path 'vendor/bundle' \
    && bundle install

COPY start.sh /start.sh
RUN chmod 744 /start.sh
CMD [ "sh", "/start.sh" ]
start.sh
#! /bin/sh

if [ "${RAILS_ENV}" = "production" ]
then
    bundle exec rails assets:precompile
fi

bundle exec rails s -p ${PORT:-3000} -b 0.0.0.0
.circleci/config.yml
version: 2.1
orbs:
  ruby: circleci/ruby@1.1.2   # config.ymlのruby向け記法を導入
  node: circleci/node@2
  heroku: circleci/heroku@1.2.3

jobs:
  build:
    docker:
      - image: circleci/ruby:2.7.4-node  
    working_directory: ~/kyodokoza/src 
    steps:
      - checkout:
          path: ~/kyodokoza
      - ruby/install-deps 

  test: 
    docker:
      - image: circleci/ruby:2.7.4-node
      - image: circleci/mysql:5.5
        environment: 
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: app_test
          MYSQL_USER: root
    environment:
      BUNDLE_JOBS: "3"
      BUNDLE_RETRY: "3"
      BUNDLE_PATH: vendor/bundle
      APP_DATABASE_HOST: "127.0.0.1" 
      RAILS_ENV: test
    working_directory: ~/kyodokoza/src
    steps:
      - checkout: 
          path: ~/kyodokoza
      - ruby/install-deps 
      - run:
          name: Database setup
          command: bundle exec rake db:create
      - run:
          name: yarn install
          command: yarn install
      - run:
          name: Database migrate
          command: bundle exec rake db:migrate RAILS_ENV=test
      - run:
          name: RSpec
          command: bundle exec rspec
  deploy:
    docker:
      - image: circleci/ruby:2.7.4
    steps:
      - checkout   
      - setup_remote_docker:
          version:  19.03.13
      - heroku/install 
      - run:
          name: heroku login
          command: heroku container:login
      - run:
          name: push docker image
          command: heroku container:push web -a $HEROKU_APP_NAME  
      - run:
          name: release docker image
          command: heroku container:release web -a $HEROKU_APP_NAME
      - run:
          name: database setup
          command: heroku run bundle exec rake db:migrate RAILS_ENV=production -a $HEROKU_APP_NAME

workflows:
  version: 2
  build_test_deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test 
          filters:
            branches:
              only: main

実装開始の前に

今回は、「ActiveJob」の「キューイングライブラリ」として「sidekicq」を使用。

「ActiveJob」って何?

Railsガイドから引用。

ActiveJobの目的は以下の通り。

Active Jobは、ジョブを宣言し、
それによってバックエンドでさまざまな方法によるキュー操作を実行するためのフレームワークです。
ジョブには、定期的なクリーンアップを始めとして、請求書発行やメール配信など、あらゆる処理がジョブになります。
これらのジョブをより細かな作業単位に分割して並列実行することもできます。

「バックグラウンドjob実行のための共通化インターフェース」ということのようです。

「sidekiq」とか「キューイングライブラリ」って何?

Railsガイドから引用。

production環境でのジョブのキュー登録と実行では、キューイングのバックエンドを用意しておく必要があります。具体的には、Railsで使うべきサードパーティのキューイングライブラリを決める必要があります。 Rails自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけです。 プロセスがクラッシュしたりコンピュータをリセットしたりすると、デフォルトの非同期バックエンドの振る舞いによって主要なジョブが失われてしまいます。アプリケーションが小規模な場合やミッションクリティカルでないジョブであればこれでも構いませんが、多くのproductionでは永続的なバックエンドを選ぶ必要があります。

そもそもActiveJobには、
定期実行や重たい処理等の「非同期な処理(=Jobの登録から実行まで時間的な隔たりがある処理)」を登録します。
その時間的隔たりの間に、プロセスクラッシュ等により、Jobが失われる可能性があります。
そうならないためにも、Jobを保管する場所を用意したい。
それがキューイングライブラリ。例えばsidekicq。
他には、ResqueやDelayedJobなどもあるらしいです。

sidekiqは公式githubのwikiが詳しいので、まずはざっと目を通すといいと思います。
https://github.com/mperham/sidekiq/wiki

実装(開発環境)

実装の流れ(開発環境)

  • gemの導入(sidekiq, sidekiq-scheduler)
  • docker-composeにredis用・sidekiq用のコンテナを導入
  • ActiveJob、sidekiqの設定
  • テスト用のジョブの設定、コンソール上で実行
  • sidekiq-schedulerの設定
  • テスト用のメール定期配信ジョブの設定、コンソール上で実行

sidekiqの導入~コンソール上で動作確認

gemの導入

gemdileに追加してbundle install

gemfile
gem 'sidekiq'
gem 'sidekiq-scheduler'  # 後々使うので、先に入れておく。

docker-composeにredis用・sidekiq用のコンテナを導入

Dockerも修正。redisサーバーをコンテナで立ち上げる。

Dokcer-compose.yml
# 以下追加
  redis:
    image: "redis:latest"
    ports:
      - "6379:6379"
    volumes:
      - "./data/redis:/data"

  sidekiq:
    build: .
    command: bundle exec sidekiq
    volumes:
      - ./src:/app
      - gem_data:/usr/local/bundle
    environment:
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis

ActiveJob、sidekiqの設定

ActiveJobのキューインライブラリとしてsidekiqを設定

config/application.rb
module App
  class Application < Rails::Application
    # 以下追加
    config.active_job.queue_adapter = :sidekiq
  end
end

sidekiqのredisとの通信を設定。
docker-compose.ymlで環境変数を定義した、REDIS_URLを使用。

config/initializer/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV["REDIS_URL"] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV["REDIS_URL"] }
end

テスト用のジョブの設定、コンソール上で実行

docker-compose up でコンテナ立ち上げした後、
動作確認用にsamplejobクラスを定義して、コンソール上で実行してみる。

console
docker-compose exec web bundle exec rails g job SampleJob
app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # テスト用の処理を記述
    puts "=====サンプルジョブです======"
  end
end

sidekiqのコンソールでSampleJobの実行

docker-compose exec sidekiq rails c
irb
[1] pry(main)> SampleJob.perform_later
Enqueued SampleJob (Job ID: 541558a6-e161-4688-bdac-af87749c0ef6) to Sidekiq(default)
=> #<SampleJob:0x0000563727fb5fb8
 @arguments=[],
 @exception_executions={},
 @executions=0,
 @job_id="541558a6-e161-4688-bdac-af87749c0ef6",
 @priority=nil,
 @provider_job_id="8dc90dd89d58f0348c963338",
 @queue_name="default",
 @timezone="Asia/Tokyo">
sidekiqのlog
2022-01-03T00:56:14.908Z pid=1 tid=gup class=SampleJob jid=8f1e338731e0576cd6376cd4 INFO: start

2022-01-03T00:56:15.857Z pid=1 tid=gup class=SampleJob jid=8f1e338731e0576cd6376cd4 INFO: Performing SampleJob (Job ID: bfccddd8-2cc8-4cd9-9c78-f7a0b628edea) from Sidekiq(default) enqueued at 2022-01-03T00:56:14Z

=====サンプルジョブです======

2022-01-03T00:56:15.859Z pid=1 tid=gup class=SampleJob jid=8f1e338731e0576cd6376cd4 INFO: Performed SampleJob (Job ID: bfccddd8-2cc8-4cd9-9c78-f7a0b628edea) from Sidekiq(default) in 0.23ms

2022-01-03T00:56:15.862Z pid=1 tid=gup class=SampleJob jid=8f1e338731e0576cd6376cd4 elapsed=0.954 INFO: done

コンソール上の動作確認ができました。
因みに、perform_laterの他に、以下のようなメソッドも使うことができます。

SampleJob.perform_async
SampleJob.perform_in(5.minutes)
SampleJob.perform_at(5.minutes.from_now)

参照: https://github.com/mperham/sidekiq/wiki/Getting-Started

sidekiq-schedulerの導入~コンソール上で動作確認

単独でsidekiqのジョブ実行の確認ができました。
次に、定期実行のためにsidekiq-schedulerを導入、テストしていきます。

sidekiq-schedulerの設定

Sidekiq-schedulerのGemは前節で導入済みのはず。
sidekiq.ymlを追加して、cron likeなジョブスケジューリングを設定。

config/sidekiq.yml
:schedule:
  sample_job: 
    cron: '0 * * * * *' # Runs once per minute
    class: SampleJob

うまくいけば、sidekiqのログにsample_jobの実行結果が表示される。

log
sidekiq_1    | 2022-01-03T01:13:00.259Z pid=1 tid=gnh INFO: queueing SampleJob (SampleJob)
sidekiq_1    | 2022-01-03T01:13:00.261Z pid=1 tid=gp5 class=SampleJob jid=b452e81949a58ad40be54307 INFO: start
sidekiq_1    | 2022-01-03T01:13:00.375Z pid=1 tid=gnh INFO: Enqueued SampleJob (Job ID: 0df2f507-3e67-4fc9-a3ee-5e768d554877) to Sidekiq(default)
sidekiq_1    | 2022-01-03T01:13:06.562Z pid=1 tid=gp5 class=SampleJob jid=b452e81949a58ad40be54307 INFO: Performing SampleJob (Job ID: 0df2f507-3e67-4fc9-a3ee-5e768d554877) from Sidekiq(default) enqueued at 2022-01-03T01:13:00Z 
sidekiq_1    | =====サンプルジョブです======
sidekiq_1    | 2022-01-03T01:13:17.173Z pid=1 tid=gp5 class=SampleJob jid=b452e81949a58ad40be54307 INFO: Performed SampleJob (Job ID: 0df2f507-3e67-4fc9-a3ee-5e768d554877) from Sidekiq(default) in 10595.9ms
sidekiq_1    | 2022-01-03T01:13:17.181Z pid=1 tid=gp5 class=SampleJob jid=b452e81949a58ad40be54307 elapsed=16.92 INFO: done

sidekiq_1    | 2022-01-03T01:14:00.220Z pid=1 tid=gnh INFO: queueing SampleJob (SampleJob)
sidekiq_1    | 2022-01-03T01:14:00.222Z pid=1 tid=gwd class=SampleJob jid=fd5771e57a45b22f2d98da82 INFO: start
sidekiq_1    | 2022-01-03T01:14:00.223Z pid=1 tid=gnh INFO: Enqueued SampleJob (Job ID: b712844c-1ba6-4774-b31d-53e857caaf95) to Sidekiq(default)
sidekiq_1    | 2022-01-03T01:14:01.788Z pid=1 tid=gwd class=SampleJob jid=fd5771e57a45b22f2d98da82 INFO: Performing SampleJob (Job ID: b712844c-1ba6-4774-b31d-53e857caaf95) from Sidekiq(default) enqueued at 2022-01-03T01:14:00Z 
sidekiq_1    | =====サンプルジョブです======
sidekiq_1    | 2022-01-03T01:14:01.791Z pid=1 tid=gwd class=SampleJob jid=fd5771e57a45b22f2d98da82 INFO: Performed SampleJob (Job ID: b712844c-1ba6-4774-b31d-53e857caaf95) from Sidekiq(default) in 0.22ms
sidekiq_1    | 2022-01-03T01:14:01.793Z pid=1 tid=gwd class=SampleJob jid=fd5771e57a45b22f2d98da82 elapsed=1.571 INFO: done

参照:
https://kerubito.net/technology/3315
https://github.com/moove-it/sidekiq-scheduler

テスト用のメール定期配信ジョブの設定、コンソール上で実行

まずは通常通り、メイラーのメソッド・ビューの記述。

app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def weekly_notification
    user = User.first
    mail  to: user,
          subject: "test mail"
  end
end
app/views/user_maier/weekly_notifications.html.erb
<p>これはテスト用メールです<p>

Jobの作成

console
docker-compose run web bundle exec rails g job WeeklyMailJob
app/jobs/weekly_mail_job.rb
class WeeklyMailJob < ApplicationJob
  queue_as :default

  def perform(*args)
    UserMailer.weekly_notification.deliver_now
    puts "==== send WeeklyMail!! ===="
  end
end

docker-compose upで確認。

letter_opener_webのgemを使えばメール配信されていることを確認できると思います。今回は割愛。

実装(本番環境)

ここからはredis,sidekiqをHerokuで使えるようにしていきます。

実装の流れ(本番環境)

  • heroku redisの導入
  • circleci.yml, Dockerfile, docker-compose.ymlの記述
  • デプロイ、ログ・メール受信の確認

参考:
Worker dyno、バックグラウンドジョブ、キューイング(Heroku公式)
https://devcenter.heroku.com/ja/articles/background-jobs-queueing

heroku redisの導入

heroku redisの導入

まずはheroku redisのadd-onを自分のアプリに導入。
コンソールからでも、ダッシュボードからでもOKです。
ダッシュボードからはこちらから↓。
https://elements.heroku.com/addons/heroku-redis

※デフォルトではredisのversionは6.2が設定される。
https://devcenter.heroku.com/articles/heroku-redis#version-support-and-legacy-infrastructure)

※Heroku上でインスタンスを生成すると、REDIS_URL環境変数は自動で上書き設定される。

If you’ve manually created a REDIS_URL config var on your app, it is overwritten when you add your first heroku-redis add-on.

参照:
https://devcenter.heroku.com/articles/heroku-redis#create-an-instance

worker dynoを導入

RailsのWEBアプリをHeroku上で動かす場合、
自動でWEBプロセス(web dyno)を作り、その中で動作している
(Herokuのdyno ≒ AWSのEC2インスタンス)。

Heoku上でsidekiqを動作させるには、web dynoと並列で、
worker dynoが必要です。

プロセスモデルについて詳細は以下を参照。
https://devcenter.heroku.com/ja/articles/process-model

ここでは、webプロセス(Railsアプリケーションのフロントエンド)とworkerプロセス(sidekiq)を用意して、
1,webプロセスがクライアントからのリクエストを受信
2,webプロセスがタスクをジョブキュー(Redis)に追加
3,workerプロセスがキューに溜まったタスクを検知し、取り出して実行
という処理を実装します。

つまり構成のイメージを図にすると、以下の通りです。

=====
web dyno(rails app)

redis

worker dyno(sidekiq)

=====

また、Dockerを使わずにデプロイする場合は
プロジェクトのルートに以下を記載したProcfileを用意して
Sidekiq用のworkerのプロセスタイプを定義し、herokuにpushすればOKです。
(参考:https://madogiwa0124.hatenablog.com/entry/2021/03/28/161255)

今回は、Dockerおよびherokuコンテナレジストリの仕組みを使ってデプロイするので、
Dockerfile,CircleCIのymlファイルを修正して、
web dyno, worker dynoを作ります。

circleci.yml, Dockerfile, docker-compose.ymlの記述

実装方針

デプロイの設定(circleci/config.ymlの記述)は以下の通り。

sidekiq導入前)
rails app <= Dockerfile
heroku container:push web app_name

導入後)
rails app <= Dockerfile.web
sidekiq <= Dockerfile.worker
heroku container:push --recursive app_name

参照:
https://devcenter.heroku.com/ja/articles/container-registry-and-runtime
https://qiita.com/sho7650/items/9654377a8fc2d4db236d

実際に、各種設定ファイルを修正します。

Dockerfile.web

ファイル名変更
Dockerfile => Dockerfile.web

Dockerfile.worker

Dockerfile.webと最後のCMD以外同じ内容です。
(本当はうまく共通化したいが、今のところ後回し。。)
(うまいやり方あれば、教えていただけますと幸いです。。)

Dockerfile.worker
FROM ruby:2.7.4

ENV RAILS_ENV=production

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update -qq \
    && apt-get install -y nodejs yarn build-essential default-mysql-client

WORKDIR /app
COPY ./src /app
RUN bundle config --local set path 'vendor/bundle' \
    && bundle install

COPY start_worker.sh /start_worker.sh
RUN chmod 744 /start_worker.sh
CMD [ "sh", "/start_worker.sh" ]

こちらも追加。

start_worker.sh
#! /bin/sh

if [ "${RAILS_ENV}" = "production" ]
then
    bundle exec rails assets:precompile
fi

bundle exec sidekiq
docker-compose.yml

以下、変更点のみ記載。

dokcer-compose.yml
# 変更前
  web: &app
    build: .

 (中略)

  sidekiq:
    build: 
      context: .
      dockerfile: Dockerfile.worker
    command: bundle exec sidekiq
 
# 変更後
  web: &app
    build: 
      context: .
      dockerfile: Dockerfile.web

 (中略)

  sidekiq:
    build: 
      context: .
      dockerfile: Dockerfile.worker
.circleci/config.yml

以下、変更点のみ記載。

.circleci/config.yml
# 変更前
  deploy:
    steps:
      - run:
          name: push docker image
          command: heroku container:push web -a $HEROKU_APP_NAME  
      - run:
          name: release docker image
          command: heroku container:release web -a $HEROKU_APP_NAME
 
# 変更後
  deploy:
    steps:
      - run:
          name: push docker image
          command: heroku container:push web worker --recursive -a $HEROKU_APP_NAME  
      - run:
          name: release docker image
          command: heroku container:release web worker -a $HEROKU_APP_NAME

本番環境で毎分メールはさすがにしんどいので、
確認用として、「5分ごとにメール設定」に変更します。

sidekiq.yml
:schedule:
  weekly_mail: 
    cron: '0 */5 * * * *' 
    class: WeeklyMailJob

デプロイ、ログ・メール受信の確認

デプロイし、本番環境で立ち上げ確認。
コンソールで「worker dynoが起動していること」と「SampleJobが実行できること」を確認。

console
# worker dynoが起動していることを確認
$ heroku ps -a kyodokoza

=== web (Free): sh /start.sh (1)
web.1: up 2022/01/08 16:56:49 +0900 (~ 8m ago)
=== worker (Free): sh /start_worker.sh (1)
worker.1: up 2022/01/08 16:54:42 +0900 (~ 10m ago)

# SampleJobが実行できることを確認
$ heroku config -a app_name
irb(main):001:0> SampleJob.perform_later
herokuのlog
2022-01-08T07:33:14.282983+00:00 app[worker.1]: =====サンプルジョブです======

また、ログを見れば、
自動で5分ごとにweekly_mailのjobが実行されていることも確認できます。

herokuのlog
2022-01-08T07:35:01.151945+00:00 app[worker.1]: ==== send WeeklyMail!! ====
(中略)
2022-01-08T07:40:00.839061+00:00 app[worker.1]: ==== send WeeklyMail!! ====
(中略)
2022-01-08T07:45:00.570713+00:00 app[worker.1]: ==== send WeeklyMail!! ====

問題なし!

あとは自分の好みにスケジュールを修正しましょう。
今回は毎週月曜の朝9時に送信されるように設定します。

sidekiq.yml
:schedule:
  weekly_mail: 
    cron: '0 0 9 * * 1' # Runs at 9:00 every Monday
    class: WeeklyMailJob

これでOK!

2
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?