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

# やりたいこと
自作の「[家計簿アプリ](https://www.kyodokoza.com/)」の中で、
定期的（今回は毎週月曜の午前9時）に「支出のまとめ」をメールで配信する。

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

https://github.com/matsu1226/kyodokoza




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

```yml: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: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" ]
```

```shell: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
```

```yml:.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ガイドから引用。

https://railsguides.jp/active_job_basics.html

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サーバーをコンテナで立ち上げる。

```yml: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を設定

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

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

```ruby: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:console
docker-compose exec web bundle exec rails g job SampleJob
```

```ruby: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: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の他に、以下のようなメソッドも使うことができます。

```ruby
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なジョブスケジューリングを設定。

```yml: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


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

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

```ruby:app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def weekly_notification
    user = User.first
    mail  to: user,
          subject: "test mail"
  end
end
```

```erb:app/views/user_maier/weekly_notifications.html.erb
<p>これはテスト用メールです<p>
```

Jobの作成

```console:console
docker-compose run web bundle exec rails g job WeeklyMailJob
```

```ruby: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）を用意して、
１，webプロセスがクライアントからのリクエストを受信
２，webプロセスがタスクをジョブキュー（Redis）に追加
３，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: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" ]

```

こちらも追加。

```shell:start_worker.sh
#! /bin/sh

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

bundle exec sidekiq
```

##### docker-compose.yml
以下、変更点のみ記載。

```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

以下、変更点のみ記載。

```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

```

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

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

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

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

```console: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]: =====サンプルジョブです======

```

また、ログを見れば、
自動で５分ごとに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時に送信されるように設定します。

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


これでOK！