やりたいこと
自作の「家計簿アプリ」の中で、
定期的(今回は毎週月曜の午前9時)に「支出のまとめ」をメールで配信する。
※完成イメージはgithubのrepositoryを参照してください。
環境
ruby 2.7.4
rails 6.1.4
アプリケーションは、Docker,circleCIを使って、Herokuにデプロイ済み。
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
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" ]
#! /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
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
gem 'sidekiq'
gem 'sidekiq-scheduler' # 後々使うので、先に入れておく。
docker-composeにredis用・sidekiq用のコンテナを導入
Dockerも修正。redisサーバーをコンテナで立ち上げる。
# 以下追加
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を設定
module App
class Application < Rails::Application
# 以下追加
config.active_job.queue_adapter = :sidekiq
end
end
sidekiqのredisとの通信を設定。
docker-compose.ymlで環境変数を定義した、REDIS_URLを使用。
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クラスを定義して、コンソール上で実行してみる。
docker-compose exec web bundle exec rails g job SampleJob
class SampleJob < ApplicationJob
queue_as :default
def perform(*args)
# テスト用の処理を記述
puts "=====サンプルジョブです======"
end
end
sidekiqのコンソールでSampleJobの実行
docker-compose exec sidekiq rails c
[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">
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なジョブスケジューリングを設定。
:schedule:
sample_job:
cron: '0 * * * * *' # Runs once per minute
class: SampleJob
うまくいけば、sidekiqのログにsample_jobの実行結果が表示される。
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
テスト用のメール定期配信ジョブの設定、コンソール上で実行
まずは通常通り、メイラーのメソッド・ビューの記述。
class UserMailer < ApplicationMailer
def weekly_notification
user = User.first
mail to: user,
subject: "test mail"
end
end
<p>これはテスト用メールです<p>
Jobの作成
docker-compose run web bundle exec rails g job WeeklyMailJob
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以外同じ内容です。
(本当はうまく共通化したいが、今のところ後回し。。)
(うまいやり方あれば、教えていただけますと幸いです。。)
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" ]
こちらも追加。
#! /bin/sh
if [ "${RAILS_ENV}" = "production" ]
then
bundle exec rails assets:precompile
fi
bundle exec sidekiq
docker-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
以下、変更点のみ記載。
# 変更前
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分ごとにメール設定」に変更します。
:schedule:
weekly_mail:
cron: '0 */5 * * * *'
class: WeeklyMailJob
デプロイ、ログ・メール受信の確認
デプロイし、本番環境で立ち上げ確認。
コンソールで「worker dynoが起動していること」と「SampleJobが実行できること」を確認。
# 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
2022-01-08T07:33:14.282983+00:00 app[worker.1]: =====サンプルジョブです======
また、ログを見れば、
自動で5分ごとにweekly_mailのjobが実行されていることも確認できます。
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時に送信されるように設定します。
:schedule:
weekly_mail:
cron: '0 0 9 * * 1' # Runs at 9:00 every Monday
class: WeeklyMailJob
これでOK!