Railsを使えば簡単にWebアプリケーションを立ち上げることができます。
例えば、Railsガイドに書かれている手順通りに実施するだけで1時間もかからずに画面を表示することができると思います。
静的なページだけではなく、scaffoldを使うことで簡単なデータ操作を一通りできる画面もサクッと作ることができます。
以前私が書いた『Rails newからproductionモードで動くようになるまで』では、1歩踏み込んでproduction
モードで動くまでの手順を書きました。
ただ、上記はproductionモードで動いているものの、プロダクト開発で使うためには他にも様々な設定が必要です。
プロダクトで使えるようにするためには、同期的な処理だけでは足りず、ほとんどの場合に非同期処理を行う仕組み(Active job)やメール配信の仕組み(Action Mailer)などが必要になります。
また、外部接続情報など環境ごとに設定値を持つようにしたり、継続的に開発できるようにCIを設定したり、誰でも同じ環境で開発できるようにどのマシンでも開発環境を再現できるようにしたり、様々なことをする必要があります。
この記事ではrails new
で作ったRailsアプリケーションに上記に書いたプロダクト開発するために必要になるであろう設定を入れる手順をまとめました。
※私個人が最近はRailsをAPIアプリケーションとしか扱っていないため、この記事ではAPIモードで構築します。
前提
- 開発環境: Docker
- ソース管理: GitHub
- CI: GitHub Actions
- 各種バージョンは下記の通り
- Ruby: 3.0
- Rails: 6.1.1→6.1.3.1
- APIモード
- 記事を書いている途中でライセンス問題が発生して動かなくなったのでバージョンアップ
- MySQL: 8.0.23
- Redis: 6.0.9
Githubにリポジトリを作る
今の時代、ソース管理は必須ですよね。GitHubに新しいリポジトリを追加します。
READMEや.gitignoreは rails new
した時に生成されるので不要です。
リポジトリができたらローカルにCloneしましょう。
% git clone git@github.com:ham0215/rails_api_base.git
Cloning into 'rails_api_base'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
% cd rails_api_base
rails_api_base % ls
LICENSE
Dockerの準備
開発環境はどのマシンでも同じ環境を再現しやすくするためにDockerで構築します。
Dockerfile
書き方は様々あると思いますが、下記のような感じにしています。
localeやvimは必須ではないのですが、コンテナ上でvimを使ったり日本語入力できるようにするために入れています。
FROM ruby:3.0.0
RUN apt-get update && apt-get install -y \
build-essential \
vim \
locales \
locales-all \
default-mysql-client \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV LANG ja_JP.UTF-8
RUN mkdir /app
WORKDIR /app
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install
COPY . .
CMD ["rails", "server", "-b", "0.0.0.0"]
Dockerfile内でGemfileとGemfile.lockは明記しており、ファイルがないとエラーになってしまうのでファイルを作っておきます。
Gemfileはrailsだけ記述しておき、Gemfile.lockは空でOKです。
source 'https://rubygems.org'
gem 'rails', '6.1.1'
ここまでできたら一度ビルドしてみます。
DOCKER_BUILDKITを指定すると少し高速化され、コンソールの表示が見やすくなるのでオススメです。
% COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build
db uses an image, skipping
WARNING: Native build is an experimental feature and could change at any time
Building api
...
Successfully built c20632ba2529ddbf7702f9df80ded5d28955d0272290c57ebbdb01b65f55b5ed
docker-compose.yml
MySQLもローカルから接続できるようにportsを指定しています。
portはデフォルトのままだと他のアプリケーションと被ることが多いので少しずらすと良いです。
version: '3.8'
services:
db:
image: mysql:8.0.23
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
ports:
- '3308:3306'
volumes:
- ./mysqlcnf.d:/etc/mysql/conf.d
- ./tmp/mysql:/var/lib/mysql
api:
tty: true
stdin_open: true
build: .
command: rails s -b 0.0.0.0
volumes:
- .:/app
environment:
DB_HOST: db
ports:
- "3001:3000"
depends_on:
- db
dbのvolumesに指定している- ./mysqlconf.d:/etc/mysql/conf.d
はMySQLの認証方法を変更するために指定しています。
[mysqld]
default_authentication_plugin=mysql_native_password
詳細は下記の記事をご覧ください。
下記の記事はGitHub Actionsの話ですが、同様のことを行っています。
rails new
次にRailsアプリケーションを作成します。
APIモードで作成するので--api
をつけています。その他不要なものはskipしています。
docker-compose run api bundle exec rails new . --database=mysql --skip-action-mailbox --skip-action-text --skip-spring --skip-turbolinks --skip-bootsnap --skip-action-cable --skip-javascript --skip-jbuilder --skip-system-test --api --skip-test --force
rails newが正常終了して必要なファイルが生成されたら、必要最低限の設定を行ってサーバーを立ち上げてみます。
まずDB接続情報の修正が必要です。
docker-composeでつけた名前がhostとして使えるので修正します。
- host: localhost
+ host: db
接続情報を直したらDBを作成して、空のschema.rbを作っておきましょう。
% docker-compose run api rails db:create
% docker-compose run api rails db:migrate
一通り設定ができたのでDockerを立ち上げてサーバーにアクセスしてみます。
下記コマンドでコンテナをバックグラウンドで立ち上げます。
"COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose up --build -d"
立ち上がったらブラウザで画面を表示してみましょう。
http://localhost:3001/
database setting
最初にデータベースのcharacter_setを確認しておきます。
データベースの設定は途中で変える場合、面倒なことになることが多いので必ず最初に確認しましょう。
MySQLに接続します。
db:createとdb:migrateを行っているのでDBとテーブルが作成されています。
% mysql -h 127.0.0.1 -P3308 -uroot
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| app_development |
| app_test |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.01 sec)
mysql> use app_development
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+---------------------------+
| Tables_in_app_development |
+---------------------------+
| ar_internal_metadata |
| schema_migrations |
+---------------------------+
2 rows in set (0.01 sec)
character_set
character_setを確認します。
mysql> show variables like '%char%';
+--------------------------+--------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.01 sec)
database.ymlに下記設定があるため、ほとんどの項目はutf8mb4
が設定されています。
encoding: utf8mb4
特にこだわりがなければutf8mb4
を使えばよいと思います。
character_set_systemだけutf8
ですが、こちらはutf8mb4
は設定できないので問題ありません。
utf8mb4
はutf8
を拡張したものです。詳細は下記をご覧ください。
照合順序
続いて照合順序を確認します。
mysql> SELECT @@collation_database;
+----------------------+
| @@collation_database |
+----------------------+
| utf8mb4_0900_ai_ci |
+----------------------+
1 row in set (0.00 sec)
MySQL8系からデフォルトの照合順序がutf8mb4_0900_ai_ci
になったようです。
下記のブログがとても分かりやすかったので参考にさせていただきました。
日本語を使う環境であれば、utf8mb4_general_ci
かutf8mb4_bin
を選ぶのが良いと思います。
この表だけで考えると英語の大文字/小文字が区別できるutf8mb4_bin
が良さそうに見えますが、文字列型でよくユニーク制約を付けるメールアドレスの比較は大文字/小文字を区別しない方が嬉しいのでutf8mb4_general_ci
が便利だったりします。
今回はcollation: utf8mb4_general_ci
に変更します。
database.ymlにcollation: utf8mb4_general_ci
を設定してDBを作り直します。
作り直すときはvolumsで指定しているtmp/mysql/
配下を消し忘れないようにご注意ください。
Dockerを起動し直して、改めてdb:createを行い変更されていることを確認します。
mysql> use app_development
Database changed
mysql> SELECT @@collation_database;
+----------------------+
| @@collation_database |
+----------------------+
| utf8mb4_general_ci |
+----------------------+
1 row in set (0.00 sec)
ログをjsonにする
Railsではデフォルトで下記のようなログが出力されます。
Started GET "/" for 172.24.0.1 at 2021-02-24 14:49:00 +0000
(0.7ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
Processing by Rails::WelcomeController#index as HTML
Rendering /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb
Rendered /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb (Duration: 11.5ms | Allocations: 406)
Completed 200 OK in 52ms (Views: 33.8ms | ActiveRecord: 0.0ms | Allocations: 2208)
開発中は上記のログで特に問題ないのですが、本番のログなどは監視ツール等に取り込みたいなどありjson形式にしたいことがあります。
そんなときに役立つgemがlogrageです。
Gemfileに追加して最低限の設定を入れてみました。
元のログも残したいので別ファイルに出力するようにしています。
設定方法の詳細はREADMEをご覧ください。
Rails.application.configure do
return if Rails.env.test?
config.lograge.enabled = true
config.lograge.keep_original_rails_log = true
config.lograge.logger = ActiveSupport::Logger.new "#{Rails.root}/log/lograge_#{Rails.env}.log"
config.lograge.formatter = Lograge::Formatters::Json.new
end
下記のようにjson形式のログも出力されるようになりました。
{"method":"GET","path":"/","format":"html","controller":"Rails::WelcomeController","action":"index","status":200,"duration":44.07,"view":28.32,"db":0.0}
RSpec, factory-bot, faker, simplecov
継続的に開発するアプリケーションにはテストコードが必須です。
テストを行うgemは様々あると思いますが、メジャーなRSpecを使うと良いと思います。
RSpec関連のgemを入れます。私がよく使うgemは下記の通り。
-
rspec-rails
- RailsでRSpecを使えるようにするgem
-
factory_bot_rails
- テストデータを生成するためのgem
-
faker
- ダミーデータ(適当な名前やメールアドレスなど)をいい感じに生成してくれるgem
-
simplecov
- テストカバレッジを出力してくれるgem
-
simplecov-json
- simplecovのテストカバレッジをjsonにしてくれるgem
では早速インストールします。
各gemのREADMEに記載されている通り、Gemfileに追加してinstallするだけです。
本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。
ここからは各Gemについてもう少し詳細に書いていきます。
rspec-rails
インストールするためのコマンドがあるので実行します。
% docker-compose exec api rails g rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
RSpecで必要なファイルが生成されます。
Ruby2.7.2からdeprecated warningがデフォルトで出力されなくなったのですが、test時には出力されたほうが嬉しいので出力されるように下記の記述を追加しました。
Warning[:deprecated] = true
factory_bot_rails
設定不要で使えますが、下記の記述を書いておくとFactoryBot.create
などのFactoryBot
を省略してcreate
だけで使えるようになるので便利です。
config.include FactoryBot::Syntax::Methods
faker
特に初期設定は不要です。
simplecov, simplecov-json
READMEを参考に設定を行います。
設定はconfig/initializers
配下に置きました。
出力は人が見やすいHTMLとプログラムで扱いやすいJsonの2種類にしています。
return unless Rails.env.test?
require 'simplecov'
require 'simplecov-json'
SimpleCov.formatters = [
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::JSONFormatter,
]
.simplecovにadd_filter
でカバレッジを取得する必要がないフォルダーを指定します。
設定ファイルやマイグレーションファイル、テストファイルを除いています。
SimpleCov.start('rails') do
add_filter 'config'
add_filter 'spec'
add_filter 'db'
end
RSpec実行
設定したのでテストを1つ書いてみます。
と言っても、まだ1つもアクションがないのでテストと一緒に作成してみます。
簡単にscaffoldを使ってUserリソース(カラムはnameのみ)を作ってみます。
RSpecやFactoryBotを導入していた後なので、controller / model / migration だけではなく、RSpecやFactoryBotのファイルも自動生成されました。
% docker-compose exec api rails g scaffold User name:string
invoke active_record
create db/migrate/20210311150016_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
invoke factory_bot
create spec/factories/users.rb
invoke resource_route
route resources :users
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke resource_route
invoke rspec
create spec/requests/users_spec.rb
create spec/routing/users_routing_spec.rb
簡単に動作させるなら何も編集しなくても良いレベルのソースが書かれているのでそのまま利用します。
まずはマイグレーションを実行してテストDBにも反映します。
% docker-compose exec api rails db:migrate
== 20210311150016 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0302s
== 20210311150016 CreateUsers: migrated (0.0303s) =============================
% docker-compose exec api rails db:test:prepare
% docker-compose exec api rspec
***********......
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User add some examples to (or delete) /app/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:4
2) /users GET /index renders a successful response
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:36
3) /users GET /show renders a successful response
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:44
4) /users POST /create with valid parameters creates a new User
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:53
5) /users POST /create with valid parameters renders a JSON response with the new user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:60
6) /users POST /create with invalid parameters does not create a new User
# Add a hash of attributes invalid for your model
# ./spec/requests/users_spec.rb:69
7) /users POST /create with invalid parameters renders a JSON response with errors for the new user
# Add a hash of attributes invalid for your model
# ./spec/requests/users_spec.rb:76
8) /users PATCH /update with valid parameters updates the requested user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:91
9) /users PATCH /update with valid parameters renders a JSON response with the user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:99
10) /users PATCH /update with invalid parameters renders a JSON response with errors for the user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:109
11) /users DELETE /destroy destroys the requested user
# Add a hash of attributes valid for your model
# ./spec/requests/users_spec.rb:120
Finished in 0.13536 seconds (files took 5.92 seconds to load)
17 examples, 0 failures, 11 pending
Coverage report generated for RSpec to /app/coverage. 14 / 29 LOC (48.28%) covered.
Coverage report generated for RSpec to /app/coverage/coverage.json. 14 / 29 LOC (48.28%) covered.
user_spec.rbのテストはすべてpendingになっているので実行できるように修正します。
テストを直す前にUserモデルのバリデーション失敗をテストするために、nameに必須制約を入れておきます。
class User < ApplicationRecord
validates :name, presence: true
end
users_specはskip
のところを修正しました。
nameの生成にはFakerを使っています。
let(:valid_attributes) {
- skip("Add a hash of attributes valid for your model")
+ { name: Faker::Name.name }
}
let(:invalid_attributes) {
- skip("Add a hash of attributes invalid for your model")
+ { name: '' }
}
# This should return the minimal set of values that should be in the headers
@@ -85,7 +85,7 @@ RSpec.describe "/users", type: :request do
describe "PATCH /update" do
context "with valid parameters" do
let(:new_attributes) {
- skip("Add a hash of attributes valid for your model")
+ { name: Faker::Name.name }
}
it "updates the requested user" do
@@ -93,7 +93,7 @@ RSpec.describe "/users", type: :request do
patch user_url(user),
params: { user: new_attributes }, headers: valid_headers, as: :json
user.reload
- skip("Add assertions for updated state")
+ expect(user.name).to eq new_attributes[:name]
end
再実行してみましたが、headerのチェックでこけてしましました。
% docker-compose exec api rspec
*.....F..F.......
Failures:
1) /users POST /create with invalid parameters renders a JSON response with errors for the new user
Failure/Error: expect(response.content_type).to eq("application/json")
expected: "application/json"
got: "application/json; charset=utf-8"
(compared using ==)
# ./spec/requests/users_spec.rb:80:in `block (4 levels) in <top (required)>'
2) /users PATCH /update with invalid parameters renders a JSON response with errors for the user
Failure/Error: expect(response.content_type).to eq("application/json")
expected: "application/json"
got: "application/json; charset=utf-8"
(compared using ==)
# ./spec/requests/users_spec.rb:114:in `block (4 levels) in <top (required)>'
Finished in 1.52 seconds (files took 5.43 seconds to load)
17 examples, 2 failures, 1 pending
調べてみるとRials6.1でresponse.content_type
で返却される値が変わったようです。
ということなので、エラーになっているテストのexpected valeueを変更します。
よく見たらcontent_typeの部分一致で比較している箇所もあったので合わせてみました。これはRSpecのテンプレートの修正漏れなのかな?
→ rspec-railsに該当箇所のプルリクを送ったらマージされたので次のバージョンからはこの事象は発生しなくなると思います。
- expect(response.content_type).to eq("application/json")
+ expect(response.content_type).to match(a_string_including("application/json"))
再実行したら全て成功しました。
モデルスペックがpendingのままですが今回は省略します。直す気がないpendingは残しておいても邪魔なので削除しておくと良いです。
% docker-compose exec api rspec
*................
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User add some examples to (or delete) /app/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:4
Finished in 1.44 seconds (files took 6.11 seconds to load)
17 examples, 0 failures, 1 pending
Coverage report generated for RSpec to /app/coverage. 28 / 30 LOC (93.33%) covered.
Coverage report generated for RSpec to /app/coverage/coverage.json. 28 / 30 LOC (93.33%) covered.
最後にRSpec実行時の最後に記載されているカバレッジをみてみましょう。
coverage/index.htmlをブラウザで開いてみると下記のようにファイルごとのカバレッジが見れます。
今回追加したuser系のファイルは100%になっているのでテストは網羅されてそうです!
coverage配下にjson形式のファイルも入っています。
simplecovが出力するカバレッジファイルはコミット不要なので.gitignore
にcoverageディレクトリを追加しておきましょう。
+ coverage
RuboCop
RuboCopはコードを静的解析してくれるGemです。
これを入れておくことでコードの記述揺れやインデントやスペースの入れ方などをルールに基づいて機械的にチェックすることができます。
例えば下記のようなチェックを行ってくれます。
# `{`の後のスペースがいらない
app/models/user.rb:13:19: C: [Correctable] Layout/SpaceInsideBlockBraces: Space between { and | detected.
(1..10).each { |n| p n }
^
# hogeが定義されているけど使われていない
app/models/user.rb:25:5: W: Lint/UselessAssignment: Useless assignment to variable - hoga.
hoge = 'hoge'
^^^^
# 最終行はreturnを書かなくても良い
app/models/user.rb:26:5: C: [Correctable] Style/RedundantReturn: Redundant return detected.
return true
^^^^^^
上記のような機械的にできるチェックをレビューで人がやるのは時間の無駄になるだけでなく、体裁ばかりに目がいってしまい本来に抽出したい複雑な不具合などに目がいかなくなってしまいます。
このような事象を回避するために静的コードチェックツールは必須と言えるでしょう。
READMEに記載されている通り、Gemfileに追加してinstallしましょう。
本番環境では必要ないものなのでdevelopmentやtestのgroupに追加しましょう。
インストールできたら早速実行してみます。
% docker-compose exec api rubocop
Inspecting 38 files
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Offenses:
.simplecov:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
SimpleCov.start('rails') do
...(略)
38 files inspected, 196 offenses detected, 183 offenses auto-correctable
196個も指摘されました・・・
様々な原因がありますが、現時点ではまだほとんど実装していないので指摘されている箇所はRailsが自動生成したファイルが多いです。
私はざっくり下記の方針で直しています。
- Railsが自動生成したファイル
- 今後触るファイル: RuboCopのルールに合わせて修正
- 今後ほぼ触らないファイル: RuboCopの対象外にする
- migrationファイルやconfigは対象外
まずはこの方針に合わせて設定ファイル(.rubocop.yml
)を設定しました。
NewCops: enable
を書いておくことで、バージョンアップで追加されるチェック項目が自動的に有効になります。
AllCops:
Exclude:
- 'bin/**/*'
- 'config/**/*'
- 'config.ru'
- 'db/**/*'
- 'Gemfile'
- 'spec/**/*'
- 'vendor/**/*'
NewCops: enable
この設定を入れて再実行すると21個まで減りました。
8 files inspected, 21 offenses detected, 19 offenses auto-correctable
残りは[修正する / チェックを無効(またはゆるく)にする]を考えながら見ていきます。
最終的に下記のチェックだけ無効にして他はコードを修正しました。
-
Style/Documentation
を無効- classの先頭にドキュメントがないとNGになる。コメントは必要な時だけ追加すれば良いと思っているので無効化。
まだコード量が少ないのでほとんど引っ掛かりませんでしたが、今後コードを追加していくと新しくNGになることがあると思います。
特にMetrics/AbcSize
やMetrics/MethodLength
は曲者です。
無理やりメソッドを分割することでNGを回避することができるのですが、可読性を考えて分割しない方がよいという判断もありえます。
RuboCopのチェックを全て正とするのではなく[修正する / チェックを無効(またはゆるく)にする]を吟味して開発速度が最大化される方向にチェック仕様をアップデートしていきましょう。
Brakeman
Brakemanはコードを静的解析して脆弱性を検知してくれるgemです。
これを入れておくことで典型的な脆弱性となり得るコードを機械的に検知することができます。
あくまで典型的なものを防げるだけですが、入れておいて損はないでしょう。
READMEに記載されている通り、Gemfileに追加してinstallするだけです。
本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。
早速実行します。
オプションはこのページに細かく記載されています。
# Rails6系なので -6 を指定
# 全てのチェック項目を実施したいので -A を指定
# 全ての警告を検知したいので -w 1 を指定
% docker-compose exec api brakeman -6 -A -w 1
...
== Overview ==
Controllers: 2
Models: 2
Templates: 0
Errors: 0
Security Warnings: 1
== Warning Types ==
Missing Encryption: 1
== Warnings ==
Confidence: High
Category: Missing Encryption
Check: ForceSSL
Message: The application does not force use of HTTPS: `config.force_ssl` is not enabled
File: config/environments/production.rb
Line: 1
1つ警告が検知されました。
productionではconfig.force_sslを有効にしろとのことです。
こちら本番環境の構成によって設定すべきかどうか違うと思いますが、今回はとりあえずtrueにしておきます。
対応不要な場合はbrakeman.ignore
というファイルを生成することでチェック対象外にできます。
- # config.force_ssl = true
+ config.force_ssl = true
これで警告はなくなりました。
% docker-compose exec api brakeman -6 -A -w 1
...
== Overview ==
Controllers: 2
Models: 2
Templates: 0
Errors: 0
Security Warnings: 0
== Warning Types ==
No warnings found
tbls
tblsはデータベースのスキーマ情報からテーブル定義のドキュメントを生成してくれるツールです。
テーブル定義に限りませんが、手動でメンテしているドキュメントは本番環境と乖離してしまうので、tblsのようにコードからリバースエンジニアリングできるツールは重要です。
こちらはDockerが提供されているのでそれを使います。
docker-composeに追記しました。
設定値の詳細はREADMEをご覧ください。
下記のように設定することでdocs/tables
配下にテーブル定義が出力されます。
+ tbls:
+ image: k1low/tbls:latest
+ volumes:
+ - .:/work
+ environment:
+ TBLS_DSN: mysql://root:@db:3306/app_development
+ TBLS_DOC_PATH: docs/tables
+ depends_on:
+ - db
早速実行してみます。 --force
をつけることでファイルがあっても上書きするようにしています。
% docker-compose run --rm tbls doc --force
Creating rails_api_base_tbls_run ... done
docs/tables/schema.svg
docs/tables/ar_internal_metadata.svg
docs/tables/schema_migrations.svg
docs/tables/users.svg
docs/tables/README.md
docs/tables/ar_internal_metadata.md
docs/tables/schema_migrations.md
docs/tables/users.md
下記のMarkdownが生成されました。
シードデータ
Dockerで環境構築していると環境を簡単にリセットすることができますが、そのたびにデータまで初期化されてしまっては面倒です。
そこで開発時にあったら便利なデータはシードデータを作っておき、いつでもロードできるようにしておくと良いです。
また、マスターデータなどを入れる際にもシードデータを作っておくと便利です。
シードデータの作成はseed-fuというgemが便利です。
Gemfileに追加してインストールしましょう。
2018年から更新されていないのでドキュメントなどが古いですが、Rails6でも問題なく使えます。
+gem 'seed-fu'
development環境用のシードデータを作ってみます。
User.seed(:id,
{ id: 1, name: 'hoge' },
{ id: 2, name: 'fuga' },
)
実行します。
% docker-compose exec api rails db:seed_fu
== Seed from /app/db/fixtures/development/users.rb
- User {:id=>1, :name=>"hoge"}
- User {:id=>2, :name=>"fuga"}
データベースに登録されました。
mysql> select * from users;
+----+------+----------------------------+----------------------------+
| id | name | created_at | updated_at |
+----+------+----------------------------+----------------------------+
| 1 | hoge | 2021-03-15 15:03:33.725453 | 2021-03-15 15:03:33.725453 |
| 2 | fuga | 2021-03-15 15:03:33.734686 | 2021-03-15 15:03:33.734686 |
+----+------+----------------------------+----------------------------+
2 rows in set (0.00 sec)
seed-fuのいいところは指定したキーが重複している場合はUpdateになるところです。
上記の場合はid
をキーとしてデータが存在している時はUpdateとして動作します。
そのため重複実行してもエラーになったり、実行ごとにデータが増加していったりすることはありません。
Security Alert / dependabot
Gemfileでインストールしているgemのバージョンを定期的にチェックし、脆弱性があるバージョンを使っていたり、新しいバージョンがある場合に通知してくれるサービスがあります。
継続的に開発していく場合、ライブラリのアップデートは必要不可欠なので検知できるように設定しておきましょう。
Security Alert
GitHubのリポジトリページのSettingsタブのSecurity & analysisから設定できます。
このページでDependabot alertsとDependabot security updatesを有効にするだけです。
有効にしておくとセキュリティーアラートがある場合にリポジトリページに下記のように表示されます。
See Dependabot alertsをクリックすると詳細ページに飛べます。
詳細ページからは脆弱性を解消するためのプルリクを生成することができ、とても便利です。
(当然ですが、脆弱性を解消するライブラリのバージョンが存在していない場合はプルリクは生成できません)
下記のようなプルリクが作られるので、内容を確認して問題なければマージしてバージョンアップ完了です
dependabot
dependabotは脆弱性有無にかかわらず、ライブラリのバージョンアップを検知してプルリクを作ってくれるサービスです。
こちらも入れておきましょう。
marketplaceからインストールします。
インストールするとSettings>Applicationsに追加されます。
設定画面から対象のリポジトリを追加しましょう。
これで新しいバージョンのライブラリを検知した場合、プルリクを勝手に作ってくれます。
CI(GitHub Actions)
ここまででRSpecやRuboCopなどをインストールして自動テストや静的解析が整ってきましたが、コードを修正するたびに手動で各種チェックツールを実行するのは現実的ではなく、実行し忘れのリスクがあります。
そこで、Pushしたときに自動実行されるようにします。
CIをするためのツールは様々ありますが、GitHubを使っているのでGitHub Actionsを使います。
Github Actionsの構文は公式ドキュメントをご覧ください。
GitHub ActionsでRailsのCIを行う記事は以前に書いたのでこちらもみていただけるとありがたいです。
今回は自動実行したいツールごとにYAMLを作りました。
Brakeman
Brakemanを実行します。
Brakemanは静的解析なのでBrakemanのGemだけインストールしています。
また、DBも必要ないのでDBセッティングもなし。
pathsやpaths-ignoreを設定設定してBrakemanに影響があるファイルが更新された時だけ実行するようにしておくと良いです。
name: Brakeman
on:
pull_request:
branches:
- 'feature/*'
- main
paths-ignore:
- README.md
- Dockerfile
- docker-compose.yml
- 'spec/**'
- 'docs/**'
jobs:
brakeman:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- name: Set up Ruby 3.0.0
uses: ruby/setup-ruby@v1.66.0
with:
ruby-version: 3.0.0
bundler-cache: true
- name: run brakeman
run: |
gem install brakeman
brakeman -6 -A -w 1
RSpec
RSpecを実行します。
GitHub ActionsでRailsからMySQL8系に接続するにも一手間必要なのですが、詳細は別記事に書いています。
name: RSpec
on:
pull_request:
branches:
- 'feature/*'
- main
paths-ignore:
- README.md
- Dockerfile
- docker-compose.yml
jobs:
rspec:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
RAILS_ENV: test
DB_HOST: 127.0.0.1
DB_PORT: 33060
services:
db:
image: mysql:8.0.23
volumes:
- mysqlconf.d:/etc/mysql/conf.d
ports:
- 33060:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
BIND-ADDRESS: 0.0.0.0
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2.3.4
- name: Set up Ruby
uses: ruby/setup-ruby@v1.64.1
with:
ruby-version: 3.0.0
bundler-cache: true
- name: bundle install
run: |
gem install bundler
bundle install --jobs 4 --retry 3 --path vendor/bundle
- name: migration
run: |
bundle exec rails db:create
bundle exec rails db:test:prepare
- name: run rspec
run: bundle exec rspec
RuboCop
RuboCopを実行します。
RuboCopは静的解析なのでRuboCopのGemだけインストールしています。
また、DBも必要ないのでDBセッティングもなし。
paths-ignoreを.rubocopのExcludeに合わせておくと良いと思います。
name: RuboCop
on:
pull_request:
branches:
- 'feature/*'
- main
paths-ignore:
- README.md
- Dockerfile
- docker-compose.yml
- 'spec/**'
- 'docs/**'
- 'db/**'
jobs:
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- name: Set up Ruby
uses: ruby/setup-ruby@v1.66.0
with:
ruby-version: 3.0.0
bundler-cache: true
- name: run rubocop
run: |
gem install rubocop
rubocop
Seed
seedを実行します。
RuboCopやRSpecのようにコード自体をチェックするものではないのですが、コード修正した時にseedを修正し忘れて壊れてしまうことが多々あるので、CIで正常に実行できるか確認するようにしています。
seedに限らず、コード修正時によく修正漏れしてしまうものがある場合はこのように機械的に検知できるようにしておくと良いです。
name: seed
on:
pull_request:
branches:
- 'feature/*'
- main
paths-ignore:
- README.md
- Dockerfile
- docker-compose.yml
jobs:
seed:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
RAILS_ENV: development
DB_HOST: 127.0.0.1
DB_PORT: 33060
services:
db:
image: mysql:8.0.23
volumes:
- mysqlconf.d:/etc/mysql/conf.d
ports:
- 33060:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
BIND-ADDRESS: 0.0.0.0
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2.3.4
- name: Set up Ruby
uses: ruby/setup-ruby@v1.66.0
with:
ruby-version: 3.0.0
bundler-cache: true
- name: bundle install
run: |
gem install bundler
bundle install --jobs 4 --retry 3 --path vendor/bundle
- name: migration
run: |
bundle exec rails db:create
bundle exec rails db:schema:load
- name: run seed_fu
run: bundle exec rails db:seed_fu
Tbls
tblsを実行します。
tblsはREAMEに下記のように書かれている通り、CIと相性が良いです。
tbls is a CI-Friendly tool for document a database, written in Go.
ドキュメントに記載されている通りdiff
というパラメーターを指定するとDBスキーマとドキュメントの差分を検出してくれます。
これを利用してドキュメントの更新漏れを検知できるようにしています。
name: Tbls
on:
pull_request:
branches:
- 'feature/*'
paths:
- 'docs/tables/*'
- 'db/**'
jobs:
tbls:
runs-on: ubuntu-latest
env:
RAILS_ENV: development
DB_HOST: 127.0.0.1
DB_PORT: 33060
services:
db:
image: mysql:8.0.23
volumes:
- mysqlconf.d:/etc/mysql/conf.d
ports:
- 33060:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
BIND-ADDRESS: 0.0.0.0
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2.3.4
- name: Set up Ruby
uses: ruby/setup-ruby@v1.66.0
with:
ruby-version: 3.0.0
bundler-cache: true
- name: apt-get
run: |
sudo apt-get update
sudo apt-get install libmysqlclient-dev jq
- name: bundle install
run: |
gem install bundler
bundle install --jobs 4 --retry 3 --path vendor/bundle
- name: migration
run: |
bundle exec rails db:create
bundle exec rails db:migrate
- name: tbls diff
uses: docker://k1low/tbls:latest
env:
TBLS_DSN: mysql://root:@db:3306/app_development
TBLS_DOC_PATH: docs/tables
with:
args: diff
Github Actionsのdependabot
Github Actionsのyamlで指定しているライブラリもガンガンバージョンアップしていくので、dependabotで検知できるようしておくと良いです。
dependabotの設定ファイル(yaml)はGitHubのリポジトリーページから生成することができます。
DependenciesタブのDependabotで"Create config file"をクリックしてください。
(執筆時点でBetaと記載されているので今後変わるかもしれません)
yamlが生成され、ブラウザ上のエディターでyamlが生成されるので、修正してコミットします。
package-ecosystemのところだけ"github-actions"に修正しました。
特に変更しませんでしたが、チェックする頻度なども指定できます。
指定できるものはドキュメントをご覧ください。
i18n
i18nは多言語化対応するための仕組みです。
Railsガイドにも記載されている通りRailsに最初から入っています。
特に多言語化対応しないとしても、エラーメッセージなどの文言はi18nの仕組みを使っておくと一元管理されて便利です。
今回はすでに作っているuserモデルのエラーメッセージを英語/日本語で出し分けられるようにしてみます。
Nameの必須チェックしか実装されておらず面白みがないのでi18nの機能をわかりやすく試すためにカスタムバリデーションも追加しました。
class User < ApplicationRecord
validates :name, presence: true
validate :ng_name
private
def ng_name
errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME'
end
end
このまま実行すると下記のようなエラーメッセージが出力されます。
ブランクのメッセージは英語になっており、カスタムエラーのメッセージはハードコーディングした通りに出ていますが項目名がNameになっています。
irb(main):001:0> User.create!(name: '')
Traceback (most recent call last):
1: from (irb):1:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: Name can't be blank)
irb(main):002:0> User.create!(name: 'NGNAME')
Traceback (most recent call last):
2: from (irb):1:in `<main>'
1: from (irb):2:in `rescue in <main>'
ActiveRecord::RecordInvalid (Validation failed: Name にNGNAMEは使えません)
まずは設定を変更して英語と日本語を使えるようにします。
今回は言語を分岐させたい箇所で明示的に指定しようと思っているので、デフォルトはenのままにしています。
I18n.config.available_locales = %i[ja en]
I18n.default_locale = :en
続いて言語ファイル(ja.ymlやen.yml)を作りますが、最初から作るのは大変なのでrails-i18nにある言語ファイルのyamlをがconfig/locales
にコピーしておきます。
コピーしてきたyamlに今回必要な項目を追加し、モデルのバリデーションを使うようにします。
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
+ ng_name: cannot be NGNAME
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
ja:
activerecord:
+ attributes:
+ user:
+ name: 名前
errors:
messages:
record_invalid: 'バリデーションに失敗しました: %{errors}'
restrict_dependent_destroy:
has_one: "%{record}が存在しているので削除できません"
has_many: "%{record}が存在しているので削除できません"
+ ng_name: に'NGNAME'は使えません
--- a/app/models/user.rb
+++ b/app/models/user.rb
def ng_name
- errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME'
+ errors.add(:name, :ng_name) if name == 'NGNAME'
end
end
それでは英語、日本語を出し分けてみます。
I18n.with_locale
で囲むことでその中で使う言語を変更することができます。
irb(main):001:1* I18n.with_locale(:en) do
irb(main):002:1* User.create!(name: '')
irb(main):003:0> end
Traceback (most recent call last):
2: from (irb):1:in `<main>'
1: from (irb):2:in `block in <main>'
ActiveRecord::RecordInvalid (Validation failed: Name can't be blank)
irb(main):004:1* I18n.with_locale(:ja) do
irb(main):005:1* User.create!(name: '')
irb(main):006:0> end
Traceback (most recent call last):
3: from (irb):3:in `<main>'
2: from (irb):4:in `rescue in <main>'
1: from (irb):5:in `block in <main>'
ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前を入力してください)
irb(main):007:1* I18n.with_locale(:en) do
irb(main):008:1* User.create!(name: 'NGNAME')
irb(main):009:0> end
Traceback (most recent call last):
3: from (irb):6:in `<main>'
2: from (irb):7:in `rescue in <main>'
1: from (irb):8:in `block in <main>'
ActiveRecord::RecordInvalid (Validation failed: Name cannot be NGNAME)
irb(main):010:1* I18n.with_locale(:ja) do
irb(main):011:1* User.create!(name: 'NGNAME')
irb(main):012:0> end
Traceback (most recent call last):
3: from (irb):9:in `<main>'
2: from (irb):10:in `rescue in <main>'
1: from (irb):11:in `block in <main>'
ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前に'NGNAME'は使えません)
config
開発を行っていると、外部環境への接続情報など環境ごとに値を出し分けたいことが多々あります。
そういうときにはconfigというGemを使うと便利です。
これを使うことで環境ごとに切り替えたい設定値をyamlに定義することができるようになります。
Gemfileに追加してインストールします。
インストールコマンドを実行すると、下記のように環境ごとの設定ファイルが生成されます。
config直下のsettingsは環境共通の設定値で、config/settings配下に環境ごとに設定値を設定します。
% docker-compose exec api rails g config:install
create config/initializers/config.rb
create config/settings.yml
create config/settings.local.yml
create config/settings
create config/settings/development.yml
create config/settings/production.yml
create config/settings/test.yml
append .gitignore
また、gitignoreにlocal
とついているyamlが除外されるようになっています。
localがついているファイルはgitに保存しないような機密情報を設定するために用意されています。
ただ、特に暗号化されるわけではなくgitignoreに追加されるだけなので管理は自分たちで行う必要があります。
機密情報は次に紹介するcredentialsが使えるのでそちらもご確認ください。
+config/settings.local.yml
+config/settings/*.local.yml
+config/environments/*.local.yml
早速動作確認をしてみます。
yamlに下記を追加しました。
# config/settings/development.yml
app:
env: devleopment
# config/settings/production.yml
app:
env: production
下記のように環境ごとに出し分けることができました。
# rails c
Loading development environment (Rails 6.1.1)
irb(main):001:0> Settings.app.env
=> "devleopment"
# RAILS_ENV=production rails c
Loading production environment (Rails 6.1.1)
irb(main):001:0> Settings.app.env
=> "production"
Credentials
1つ前に紹介したconfigと同様に環境ごとの設定値を格納できるのですが、こちらは暗号化して保存されます。
暗号化されており値の確認や編集に一手間かかるので機密情報かどうかでconfigと使い分けると良いと思います。
こちらは最初からRailsに入っている仕組みなのでGemの追加は不要です。
早速環境ごとに作ってみます。
下記のようなエラーが発生した場合はEDITORという環境変数に編集に使うエディターを指定して実行ください。
% docker-compose exec api rails credentials:edit --environment development
No $EDITOR to open file in. Assign one like this:
EDITOR="mate --wait" bin/rails credentials:edit
For editors that fork and exit immediately, it's important to pass a wait flag,
otherwise the credentials will be saved immediately with no chance to edit.
私はvimを使うのでdocker-compose.ymlの環境変数のところに下記のように追加しました。
environment:
DB_HOST: db
+ EDITOR: vim
改めてeditコマンドを実行するとファイルが生成されるので下記の項目を記載しました。
secret:
key: secret!!
--environment development
を指定したのでdevelopment環境用の設定ファイル(暗号化済み)と復号するためのkeyが生成されます。
- config/credentials/development.key
- config/credentials/development.yml.enc
またこのときに.gitignoreに/config/credentials/development.key
が追加されます。
keyが流出してしまうと誰でも復号できるようになってしまうので、GitHubには上げずに厳重に管理しましょう。
設定値は下記のように利用することができます。
こちらもSettingsと同様に環境ごとに出し分けることができます。environmentを指定して必要な環境分用意しておきましょう。
irb(main):005:0> Rails.application.credentials.secret[:key]
=> "secret!!!"
Active Job
開発していると実行に時間がかかる処理やリアルタイムで行う必要がない処理などがでてきます。
そのような処理を非同期で実行する仕組みがActive Jobです。
詳細はRailsガイドを参照してください。
Active Jobを使うにはキューとキューに接続するためのフレームワークを使います。
様々なものがありますが、今回は幅広く使われているSidekiqを使いたいと思います。
またキューを保持するためにRedisを使います。
Redis
Redisをdocker-composeに追加します。
下記を追加してビルドし直します。PORTは他とかぶらないように少しずらしました。
+ redis:
+ image: redis:6.0.9
+ ports:
+ - '6380:6379'
起動できたらコンテナに入って動作確認しておきましょう。
% docker-compose exec redis bash
root@86983cd92065:/data# redis-cli -h redis -p 6379
redis:6379> INFO
# Server
redis_version:6.0.9
...
Sidekiq
Sidekiqをインストールします。
ドキュメントが充実しているので参考にします。
まず、Gemfileに追加してbundle install
してRedisの設定をします。
詳細はドキュメントを参照してください。
今回はinitializerを使います。
内容はドキュメント通りですが、テストのときは使わないのでreturnを入れています。
return if Rails.env.test?
Sidekiq.configure_server do |config|
config.redis = { url: 'redis://redis:6379/0' }
end
Sidekiq.configure_client do |config|
config.redis = { url: 'redis://redis:6379/0' }
end
続いてActive Jobで使えるようにします。
詳細はドキュメントを参照してください。
テスト以外の環境ではsidekiqを使うのでapplication.rbに追加して、テスト環境だけ:test
を設定しました。
config.active_job.queue_adapter = :sidekiq
config.active_job.queue_adapter = :test
テストアダプターを利用するとジョブを同期実行してくれるようになります。
テストのときに非同期実行されると結果の確認が困難なのでtestの場合はテストアダプターを設定しておくと便利です。
テストジョブ実行
一通り設定は終わったので動作確認用にテストジョブを作って実行します。
動作確認したいだけなのでログを出力するだけのジョブにします。
class ExampleJob < ActiveJob::Base
# Set the Queue as Default
queue_as :default
def perform(*args)
Rails.logger.debug 'start job!!!!'
end
end
続いてsidekiqを起動します。
% docker-compose exec api sidekiq -q default
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
minitest (>= 5.1)
Available/installed versions of this gem:
- 5.14.4
- 5.14.2
racc (~> 1.4)
Available/installed versions of this gem:
- 1.5.2
- 1.5.1
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
2021-04-01T08:43:01.144Z pid=38 tid=6hq INFO: Booting Sidekiq 6.2.0 with redis options {:url=>"redis://redis:6379/0"}
m,
`$b
.ss, $$: .,d$
`$$P,d$P' .,md$P"'
,$$$$$b/md$$$P^'
.d$$$$$$/$$$P'
$$^' `"/$$$' ____ _ _ _ _
$: ,$$: / ___|(_) __| | ___| | _(_) __ _
`b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` |
$$: ___) | | (_| | __/ <| | (_| |
$$ |____/|_|\__,_|\___|_|\_\_|\__, |
.d$$ |_|
2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Booted Rails 6.1.3.1 application in development environment
2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Running in ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]
2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: See LICENSE and the LGPL-3.0 for licensing details.
2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
2021-04-01T08:43:02.273Z pid=38 tid=6hq INFO: Starting processing, hit Ctrl-C to stop
起動したらコンソールからジョブを実行してみます。
irb(main):001:0> ExampleJob.perform_later
Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default)
=> #<ExampleJob:0x000056020c20d690 @arguments=[], @job_id="c578b673-7b66-43e4-91ac-d48f34537ccb", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="24ee1022e0a460169097b15a">
結果はログで確認します。
Sidekiq経由でジョブが実行され、きちんとログが出力されています。
[ActiveJob] Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default)
[ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performing ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) enqueued at 2021-04-01T08:44:50Z
[ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] start job!!!!
[ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performed ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) in 0.67ms
Action Mailer
アプリケーションからメールを送信したいことがあると思います。
Railsにはメールを送信する仕組みも入っているので設定しておきます。
詳細はRailsガイドを参照してください。
SendGrid
Action Mailerを設定する前にメール送信するためには配信サービスを使う必要があります。
一昔前であれば自分でメールサーバーを立てるなどかなり手間だったのですが、現状はSaasサービスを使ってサクッと配信できるのでそれを利用します。
今回は執筆時点(2021/04)で月に12,000通まで無料でメール配信できるSendGridを利用します。
SendGridは値段がお手頃なだけではなく、Ruby用のGemが用意されているのでRubyとの相性も良いです。
Sidekiq
Sidekiqを使ってメール送信を非同期で行うことができます。
ドキュメントに記載されている通りデフォルトではmailers
というキューに格納されるのでSidekiqを起動するときにmailersというキューも立ち上げるようにしましょう。
docker-dompose exec api sidekiq -q default -q mailers
ちなみにconfig/application.rbなどでconfig.action_mailer.deliver_later_queue_name = 'hoge'
のように設定することでキューをmailers
から変更することも可能です。
また、nilを設定するとdefaultのキューが使われるようになるようです。
Action Mailerの設定
Action Mailerを設定します。
ActionMailerは配信メソッドをdelivery_method
で指定することができます。
指定できるものはドキュメントを御覧ください。
SendGridをSMTPとして使うだけであれば、delivery_methodにSMTPを設定するだけでできます。
SendGridのドキュメントにもSMTPで設定する方法が記載されています。
ただ、この方法ではSendGridの機能を活かすことができないので、SendGridの機能を活かせるようにdelivery_methodに指定できるカスタムメソッドを作成したいと思います。
Railsではadd_delivery_methodという配信メソッドを追加する仕組みが用意されているため、SendGridを使う配送メソッドを独自に実装してdelivery_methodで使えるようにします。
SendGridの配送メソッド作成
それではSendGridの配送メソッドを作成します。
ゼロから作るのは難しいので、SMTPのソースを参考に作成します。
- SMTPのソースをadd_delivery_methodしている箇所
ソースの一部を抜粋しました。
smtpが指定された場合に使うクラスMail::SMTP
とパラメーター(address〜enable_starttls_auto)を指定しています。
add_delivery_method :smtp, Mail::SMTP,
address: "localhost",
port: 25,
domain: "localhost.localdomain",
user_name: nil,
password: nil,
authentication: nil,
enable_starttls_auto: true
- SMTPのソース
ソースのうち、publicメソッドを抜粋しました。
下記の2つのメソッドが定義されています。
initialize
では、add_delivery_methodで指定した引数を受け取っています。
deliver!
では、mailオブジェクトが渡されるので配送処理を実装します。
def initialize(values)
self.settings = DEFAULTS.merge(values)
end
def deliver!(mail)
response = start_smtp_session do |smtp|
Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail)
end
settings[:return_response] ? response : self
end
これらを参考に配送メソッドを作成しました。app/serviseに置きましたが場所はどこでもOKです。
配送処理はsendgrid-rubyのREADMEを参考にしています。
contentを複数指定したかったので、細かいところはmailクラスをのぞいてみて実装しています。
https://github.com/sendgrid/sendgrid-ruby/blob/main/lib/sendgrid/helpers/mail/mail.rb
class SendGridService
attr_reader :api_key, :mail
def initialize(settings)
@api_key = settings[:api_key]
end
def deliver!(mail)
@mail = mail
sg = SendGrid::API.new(api_key: api_key)
response = sg.client.mail._('send').post(request_body: request_body)
raise response.inspect if response.status_code.to_i >= 300
response.body
end
private
def request_body
sg_mail = SendGrid::Mail.new
sg_mail.from = SendGrid::Email.new(email: mail.from.first)
personalization = SendGrid::Personalization.new
mail.to.each { personalization.add_to(SendGrid::Email.new(email: _1)) }
sg_mail.add_personalization(personalization)
sg_mail.subject = mail.subject
mail.body.parts.each do
content_type = "#{_1.main_type}/#{_1.sub_type}"
sg_mail.add_content(SendGrid::Content.new(type: content_type, value: _1.body.raw_source))
end
sg_mail.to_json
end
end
上記の配送クラスをadd_delivery_methodを使って追加します。
api_keyはSendgridで発行したものを使います。環境ごとに違う&機密情報なのでcredentialsを使いました。
ちなみにto_prepareを使うとproductionでは初回のみしか読み込まれませんが、developmentでは都度読み込まれるので開発時に便利です。
Rails.configuration.to_prepare {
ActionMailer::Base.add_delivery_method(:sendgrid,
SendGridService,
api_key: Rails.application.credentials.sendgrid_api_key)
}
追加できたのでdelivery_methodで指定します。
ついでにconfig.action_mailer.raise_delivery_errors = true
を設定しておくとメール配信時にエラーが発生したときにエラー検知できるようになるので設定しておきましょう。
config.action_mailer.delivery_method = :sendgrid
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
テストの場合はメールを送りたくないので:testを指定しておきましょう。
testを指定するとメール配信される代わりにActionMailer::Base.deliveries
に格納されるためテストのときに便利です。
メール配信
設定が終わったのでメールクラスを実装します。
rails g mailerで必要なファイルを生成します。
今回はUserMailer
にしました。
% docker-compose exec api rails g mailer UserMailer hello
create app/mailers/user_mailer.rb
create app/mailers/application_mailer.rb
invoke erb
create app/views/user_mailer
create app/views/layouts/mailer.text.erb
create app/views/layouts/mailer.html.erb
create app/views/user_mailer/hello.text.erb
create app/views/user_mailer/hello.html.erb
invoke rspec
create spec/mailers/user_mailer_spec.rb
create spec/fixtures/user_mailer/hello
create spec/mailers/previews/user_mailer_preview.rb
テストメールを送りたいだけなのでサクッとmailerクラスとviewを作ります。
viewはRailsガイドに書いてあるとおりhtmlとtext形式を用意しました。
自動生成されたviewファイルに.en
をつけていますが、ここを言語の数だけ用意するだけでlocaleの設定によってメールを出し分けてくれます。
今回は動作確認をしたいだけなのでdefault_localeにしているenだけ準備しました。
class UserMailer < ApplicationMailer
def hello(user_id)
@user = User.find user_id
# usersテーブルにemailカラム追加
mail(to: user.email, subject: 'Hello!! ham!!' )
end
end
hello!! <%= @user.name %>.<br>
first mail!!!<br>
hello!! <%= @user.name %>.
first mail!!!<br>
準備ができたのでRails consoleを使って動作確認してみます。
irb(main):002:0> UserMailer.hello(1).deliver_now
User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
Rendering layout layouts/mailer.html.erb
Rendering user_mailer/hello.en.html.erb within layouts/mailer
Rendered user_mailer/hello.en.html.erb within layouts/mailer (Duration: 0.1ms | Allocations: 6)
Rendered layout layouts/mailer.html.erb (Duration: 2.6ms | Allocations: 82)
Rendering layout layouts/mailer.text.erb
Rendering user_mailer/hello.en.text.erb within layouts/mailer
Rendered user_mailer/hello.en.text.erb within layouts/mailer (Duration: 0.1ms | Allocations: 4)
Rendered layout layouts/mailer.text.erb (Duration: 3.7ms | Allocations: 80)
UserMailer#hello: processed outbound mail in 18.5ms
Delivered mail 60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail (623.8ms)
...(中略)
<60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail>>, <Subject: Hello!! ham!!>, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary="--==_mimepart_60730ef18e808_14c21fc2489e"; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>>
メールが届くことを確認しました。
hello!! ham.
first mail!!!
また、Sidekiqを起動して非同期で遅れることも確認します。
% docker-compose exec api sidekiq -q default -q mailers
非同期送信
irb(main):003:0> UserMailer.hello(1).deliver_later
Enqueued ActionMailer::MailDeliveryJob (Job ID: af499814-b119-4a69-b6db-d37935b5e650) to Sidekiq(mailers) with arguments: "UserMailer", "hello", "deliver_now", {:args=>[1]}
=> #<ActionMailer::MailDeliveryJob:0x00005651c0d9bbc0 @arguments=["UserMailer", "hello", "deliver_now", {:args=>[1]}], @job_id="af499814-b119-4a69-b6db-d37935b5e650", @queue_name="mailers", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="88ff5493a88556145308df57">
Sidekiqのコンソール
2021-04-11T15:02:46.229Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 INFO: start
2021-04-11T15:02:47.251Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 elapsed=1.054 INFO: done
Sidekiqを経由してもメール送信できることを確認できました。
Active Storage
アプリケーションからファイルをアップロードしたいことがあると思います。
Railsにはファイルアップロードの仕組みも入っているので設定しておきます。
詳細はRailsガイドを参照してください。
セットアップ
Railsガイドに記載されている通り、インストールコマンドを実行します。
% docker-compose api rails active_storage:install
実行すると、Active Storageで使うテーブルを作成するマイグレーションファイルが生成されるので実行しておきます。
config/storage.yml
を参照するとAWSやGCPを保存先にするサンプルがコメントされています。
ここの設定値を環境に合わせて変更することで外部サービスのストレージと簡単に連携することができます。
また、アップロードするファイルに対するバリデーションを行うためのGemもあるためインストールしておきます。
今回は開発環境を構築しているので外部ストレージを使わずローカル保存で動作確認します。
ファイルアップロード実装
Userモデルに画像を添付できるようにしてみます。
active_storage_validationsも入れたので、画像以外のファイルやサイズが4MB以上のファイルはアップロードできないようにバリデーションを追加してみました。
has_one_attached :avatar
validates :avatar, content_type: %i[png jpg jpeg], size: { less_than: 4.megabytes }
次にアップロードするエンドポイントを追加します。
- resources :users
+ resources :users do
+ post :avatar
+ end
routesを確認してみます。
今回追加したファイルアップロード用のエンドポイントuser_avatar POST /users/:user_id/avatar(.:format)
が追加されています。
また、/rails/active_storage
で始まるActive Storage用のエンドポイントがいくつか追加されています。
% docker-compose exec api rails routes
Prefix Verb URI Pattern Controller#Action
user_avatar POST /users/:user_id/avatar(.:format) users#avatar
users GET /users(.:format) users#index
POST /users(.:format) users#create
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show
rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show
GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show
rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show
rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show
GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
アップロード処理を実装します。
受け取ったファイルデータをattachするだけです。とても簡単ですね。
def avatar
user = User.find params[:user_id]
user.avatar.attach params[:avatar]
raise 'バリデーションエラー' if user.invalid?
head :ok
end
動作確認
動作確認します。
APIにリクエストを送れるツールなら何でも良いのですが、今回はChromeの拡張機能にあるTalend API Testerを使いました。
rails consoleでファイル添付されていることをattached?で確認して参照用のURLを発行します。
irb(main):001:0> user = User.first
irb(main):002:0> user.avatar.attached?
=> true
irb(main):003:0> Rails.application.routes.url_helpers.rails_storage_proxy_path(user.avatar, only_path: true)
=> "/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg"
早速発行したURLにブラウザでアクセスしてみると、下記のようなエラーが発生しました。
NoMethodError (undefined method `flash' for #<ActionDispatch::Request GET "http://localhost:3001/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg" for 172.23.0.1>):
APIモードだとflashが使えないのでflashが見つからずエラーになっているようです。
下記を追加して使えるようにします。
config.middleware.use ActionDispatch::Flash
おまけ
参照URLの/rails/active_storage
を変える
Active Storageで自動追加されるURLのprefixは/rails/active_storage
になります。
システム名が丸見えでこのまま使いたいと思う人は少ないと思います。
変更するための仕組みが用意されているので変更しておきましょう。
config.active_storage.routes_prefix = '/attachments'
のように設定することで/rails/active_storage
を変更することができます。
上記のように設定すると下記のように変わります。
% docker-compose exec api rails routes
rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show
rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show
GET /attachments/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show
rails_blob_representation GET /attachments/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show
rails_blob_representation_proxy GET /attachments/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show
GET /attachments/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show
rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /attachments/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /attachments/direct_uploads(.:format)
使わないエンドポイントを消す
Active Storageのエンドポイントがいくつか定義されますが、アプリケーションで全てを使うことはないと思います。
使わないエンドポイントは閉じておきたいので、エンドポイントを消す手順を調べてみました。
config.active_storage.draw_routes = false
を設定することまるごと削除することができるのですが、一部だけ消す方法は見当たりませんでした。
そこで、railsのソースから必要な箇所だけ持ってくることにしました。
まずはconfig.active_storage.draw_routes = false
を設定してデフォルトで追加されるルートを削除します。
config.active_storage.draw_routes = false
次にconfig/routes.rb
にRailsのソースから必要な箇所を転記します。
参照で使うエンドポイントだけ残まして他は削除しました。
以下差分です。長くなるのでscope以下は省略しています。
#
# 下記から必要な箇所を転記
# https://github.com/rails/rails/blob/v6.1.3.1/activestorage/config/routes.rb
#
scope ActiveStorage.routes_prefix do
get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob
get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy
get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
end
...(長いので省略)
routesを確認すると減っていることが確認できます。
% docker-compose exec api rails routes
...(前略)
rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show
rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show
rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show
今回はRailsのソースコードをコピペして対応しました。
このように対応するとRailsをバージョンアップしたときなどオリジナルのソースが更新されてしまったときにコピペした部分にも反映が必要となります。
コピペ対応はバージョンアップ時のバグの温床になりやすいので可能な限りやらないほうが良いです。
どうしてもやる場合は、下記のようにコピペした箇所にバージョンが変わった場合にエラーになる処理を追加しておきましょう。
raise 'バージョンアップしたので下記コピペコードも確認' unless ActiveStorage.version.to_s == '6.1.3.1'
これを入れておくとバージョンが変わるとエラーが発生するようになり、バージョンアップ時に反映漏れを防げるようになります。
/app/config/routes.rb:10:in `block in <top (required)>': バージョンアップしたので下記コピペコードも確認 (RuntimeError)
その他のおすすめ機能
ここまで私がほぼ外せないと思っている機能を設定してきました。
この章では必須ではないが、便利だと思っている機能を紹介します。
devise
deviseはRailsに認証を追加するために便利なgemです。
最近では認証・認可はIDaaSを利用することも多いですが、そこまでお金をかけられない場合にサクッと認証を導入するときに便利です。
deviseにはメール送信する処理もあるのですが、同期処理で送信しています。
deviseのメールを非同期にするgem(devise-sync)もあるので合わせて使うと便利です。
さらに私は使ったことはないですが、deviseに2要素認証を入れるgem(devise-two-factorもあるようです。
最近では多要素認証が主流になってきているのでチェックしておきたいです。
JWT
JWTは認証用トークンなどで幅広く使われているトークンです。
Amazon CognitoやAzure ADなどでも使われています。
JWT自体の仕組みについてはこの記事には書きませんが、JWTはトークン自体に様々な情報をもたせることができるのでとても便利です。
Rubyで使うためのGem(ruby-jwt)もあるため簡単に導入することができます。
認証を実装するときは候補の1つとしておくと良いと思います。
graphql-ruby
RailsでGraphQLを使うためのGemです。
GraphQLはRESTに変わるAPIのインターフェースで、クエリーを指定することで呼び出し側からレスポンスをカスタマイズすることができます。
詳細は公式ドキュメントを御覧ください。
Rails単体で見ると入出力のバリデーションを自動で行ってくれたり、スキーマファイルが自動生成されスキーマファーストの開発がやりやすいなどメリットがありますが、GraphQLの最大のメリットは様々なプログラミング言語に広がっている点だと思います。
マイクロサービス化が主流となっている昨今ではサービス間でプログラミング言語が異なることがよくあります。
そのため、様々な言語でGraphQLのフレームワークが提供されていることはシステム連携する上で大きなメリットだと思います。
初見ではとっつきにくいとこもありますが、新たにAPIを作成する場合は一度検討してみてください。
開発環境完成
かなり長くなりましたが、ここまでの手順でローカルで動作する開発環境がやっと完成しました。
開発時に便利(というか複数人で開発するときは必須)であるCIの設定や、非同期処理やメール配信、環境ごとの設定変更などプロダクトとして利用するアプリケーションを開発するときにはほぼ100%必要となる機能は一通り入れることができたと思います。
この他にも最後に紹介したdeviseやgraphqlなど開発するプロダクトに合わせて必要なものを追加していきましょう。
今回はプロダクション環境での実行については触れませんでしたが、プロダクションで動かすにはインフラやトラフィックに合わせたチューニングなどがさらに必要になります。
ここまでも長かったですが、まだまだここがプロダクト開発のスタート位置です!
作ったプロダクトを継続的にどんどん育てていきましょう!!