🚀
Rails7 API × OpenAPI × Rswag × Docker の環境構築方法
はじめに
Ruby を使った API 開発に必要な最小限の構成での環境構築方法をまとめてみました。
環境
- Ruby:
3.2.2
- Rails:
7.0.6
- MySQL:
8.0
コード
ライブラリ
active_interaction
- command パターンでアプリケーション固有のビジネスロジックを実装できます。
Panko Serializers
- API のレスポンス(JSON) の形式を整えて扱いやすくするための gem です。
anyway_config
- 設定管理系の gem です。
enumerize
- モデルで enum(列挙型) を使う時に便利な gem です。
faker
- ダミーデータを手軽に作成できる gem です。
rswag
- テストファーストな API 開発ができる gem です。
- RSpec から swagger.yml を生成できるようになります。
seed-fu
- 開発時のデータ作成に使っています。
json-schema_builder
- rswag-specs でリクエスト、レスポンスデータの共通化に利用しています。
事前準備
$ mkdir demo-api
$ cd demo-api
Gemfile を作成します。
$ bundle init
Rails を有効にします。
Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
-# gem "rails"
+gem "rails"
Docker のビルドに必要なので空の Gemfile.lock を作成します。
$ touch Gemfile.lock
Docker 用のファイルの作成
Dockerfile
FROM ruby:3.2.2
RUN apt update -qq && apt install -y --no-install-recommends vim
ENV LANG=C.UTF-8
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install # rails new が終わったら削除する
COPY containers/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3.4'
services:
app:
build: .
image: demo-api:1.0.0
command: /bin/sh -c "rm -f tmp/pids/server.pid && rails server -p 3000 -b '0.0.0.0'"
volumes:
- .:/app
- bundle:/usr/local/bundle
environment:
- RAILS_ENV=development
- APP_DEFAULT_URL_HOST=localhost
- APP_DEFAULT_URL_PORT=3000
- DB_DATABASE_HOST=mysql
- DB_DATABASE_USER=root
- DB_DATABASE_PASSWORD=
depends_on:
mysql:
condition: service_healthy
links:
- mysql
ports:
- '3000:3000'
tmpfs:
- /tmp
stdin_open: true
tty: true
mysql:
image: mysql:8.0
platform: linux/amd64
volumes:
- ./containers/mysql:/etc/mysql/conf.d/
- mysql:/var/lib/mysql
healthcheck:
test: mysqladmin ping -h mysql -P 3306 -u root
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
ports:
- '4306:3306'
volumes:
bundle:
mysql:
containers/mysql/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
[mysql]
default-character-set=utf8mb4
[client]
default-character-set=utf8mb4
Dockerfile 内で指定する Entrypoint 用のシェルファイルを用意します。
Quickstart: Compose and Rails を参考にしました。
containers/entrypoint.sh
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
Rails プロジェクトの作成
$ docker-compose run app rails new . --force --api -d mysql --minimal --skip-test --skip-bundle
-
--force
: ファイルが存在する場合に上書きします。 -
--api
: API モードで Rails アプリケーションを生成します。 -
-d mysql
: MySQL を使用するようにします。 -
--minimal
: 最小限の Rails アプリケーションを生成します。 -
--skip-test
: RSpec を使用するので Minitest を生成しないようにします。 -
--skip-bundle
: 後で Gemfile を変更するのでここでは bundle をスキップします。
Makefile の作成
Makefile
RUN := run --rm
DOCKER_COMPOSE_RUN := docker-compose $(RUN)
args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
init:
@make build
@make bundle.install
@make db.create
@make db.migrate
@make db.seed
build:
docker-compose build
rebuild:
docker-compose build --force-rm --no-cache
up:
docker-compose up
upd:
docker-compose up -d
down:
docker-compose down
attach:
docker attach (docker-compose ps -q app)
bash:
${DOCKER_COMPOSE_RUN} app bash
bundle.install:
${DOCKER_COMPOSE_RUN} app bundle install
bundle.update:
${DOCKER_COMPOSE_RUN} app bundle update
console:
${DOCKER_COMPOSE_RUN} app rails c
db.console:
${DOCKER_COMPOSE_RUN} app rails dbconsole
db.create:
${DOCKER_COMPOSE_RUN} app rails db:create
${DOCKER_COMPOSE_RUN} app rails db:create RAILS_ENV=test
db.migrate:
${DOCKER_COMPOSE_RUN} app rails db:migrate
${DOCKER_COMPOSE_RUN} app rails db:migrate RAILS_ENV=test
db.seed:
${DOCKER_COMPOSE_RUN} app rails db:seed
db.reset:
${DOCKER_COMPOSE_RUN} app rails db:migrate:reset
@make db.seed
annotate:
${DOCKER_COMPOSE_RUN} app annotate
rails:
${DOCKER_COMPOSE_RUN} app rails $(args)
routes:
${DOCKER_COMPOSE_RUN} app rails routes
rspec:
${DOCKER_COMPOSE_RUN} -e RAILS_ENV=test app rspec $(args)
rubocop:
${DOCKER_COMPOSE_RUN} app rubocop $(args)
rubocop.fix:
${DOCKER_COMPOSE_RUN} app rubocop -a
brakeman:
${DOCKER_COMPOSE_RUN} app brakeman
down.all:
if [ -n "`docker ps -q`" ]; then docker kill `docker ps -q`; fi
docker container prune -f
swagger.create:
${DOCKER_COMPOSE_RUN} -e RAILS_ENV=test app rspec --format Rswag::Specs::SwaggerFormatter --order defined --pattern 'spec/requests/**/*_spec.rb'
credential.edit:
${DOCKER_COMPOSE_RUN} -e EDITOR=vim app rails credentials:edit -e $(args)
必要な gem の追加
すべて置き換えます。
Gemfile
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.2.2'
gem 'rails', '~> 7.0.5'
gem 'active_interaction', '~> 5.2'
gem 'anyway_config', '~> 2.0'
gem 'enumerize'
gem 'mysql2', '~> 0.5'
gem 'panko_serializer'
gem 'puma', '~> 5.0'
gem 'rack-cors'
gem 'rswag'
gem 'seed-fu', '~> 2.3'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
group :development, :test do
gem 'bullet'
gem 'debug', platforms: %i[mri mingw x64_mingw]
gem 'factory_bot_rails'
gem 'faker'
gem 'gimei'
end
group :production, :staging do
gem 'aws-healthcheck'
end
group :development do
gem 'annotate'
gem 'brakeman'
gem 'rubocop', require: false
gem 'rubocop-performance'
gem 'rubocop-rails'
gem 'rubocop-rspec'
gem 'solargraph'
end
group :test do
gem 'json-schema_builder'
gem 'rspec-rails'
end
$ make bundle.install
RuboCop の設定ファイルの追加
.rubocop.yml
require:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
AllCops:
NewCops: enable
DisplayCopNames: true
TargetRubyVersion: 3.2
Include:
- '**/*.rake'
- '**/*.rb'
- 'Gemfile'
Exclude:
- 'bin/**/*'
- 'config/boot.rb'
- 'config/environment.rb'
- 'config/environments/**/*'
- 'config/puma.rb'
- 'db/schema.rb'
- 'vendor/**/*'
SuggestExtensions: false
Rails:
Enabled: true
############################################################
#################### Style ################################
############################################################
Style/AndOr:
EnforcedStyle: conditionals
Style/AsciiComments:
Enabled: false
Style/BarePercentLiterals:
EnforcedStyle: percent_q
Style/ClassAndModuleChildren:
Enabled: false
Style/ClassCheck:
EnforcedStyle: is_a?
Style/Documentation:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/FormatString:
EnforcedStyle: format
Style/FrozenStringLiteralComment:
SafeAutoCorrect: true
Style/GuardClause:
Enabled: false
MinBodyLength: 3
Style/IfUnlessModifier:
Enabled: false
Style/Lambda:
EnforcedStyle: literal
Style/RedundantReturn:
Enabled: false
Style/RedundantSelf:
Enabled: false
Style/SignalException:
EnforcedStyle: only_raise
############################################################
###################### Lint ################################
############################################################
Lint/EmptyBlock:
Exclude:
- 'spec/models/**/*'
- 'spec/factories/**/*'
############################################################
###################### Layout ##############################
############################################################
Layout/LineLength:
Max: 120
Exclude:
- 'db/fixtures/**/*'
- 'spec/**/*'
Layout/SpaceAroundEqualsInParameterDefault:
EnforcedStyle: space
Layout/SpaceInsideBlockBraces:
EnforcedStyle: space
EnforcedStyleForEmptyBraces: no_space
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space
EnforcedStyleForEmptyBraces: no_space
Layout/TrailingEmptyLines:
EnforcedStyle: final_newline
Layout/EmptyLineBetweenDefs:
AllowAdjacentOneLineDefs: true
Layout/MultilineMethodCallIndentation:
EnforcedStyle: aligned
############################################################
#################### Naming ################################
############################################################
Naming/VariableNumber:
EnforcedStyle: snake_case
############################################################
#################### Metrics ###############################
############################################################
Metrics/AbcSize:
Max: 30
Exclude:
- 'db/migrate/**/*'
Metrics/BlockLength:
CountComments: false
Max: 25
Exclude:
- 'config/routes.rb'
- 'spec/**/*'
Metrics/BlockNesting:
Max: 3
Metrics/ClassLength:
Max: 150
Exclude:
- 'spec/**/*'
Metrics/CyclomaticComplexity:
Max: 6
Metrics/MethodLength:
Max: 25
Exclude:
- 'db/migrate/*.rb'
- 'spec/schemas/**/*'
Metrics/ModuleLength:
Max: 100
Exclude:
- 'spec/**/*'
Metrics/ParameterLists:
Max: 5
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 9
############################################################
###################### RSpec ###############################
############################################################
RSpec/ContextWording:
Enabled: false
RSpec/EmptyExampleGroup:
Exclude:
- 'spec/models/**/*'
RSpec/ExampleLength:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Max: 7
RSpec/NamedSubject:
Enabled: false
RSpec/NestedGroups:
Max: 6
RSpec/VariableName:
Exclude:
- 'spec/requests/**/*'
EnforcedStyle: snake_case
Panko Serializers 用のファイルの作成
app/serializers/application_serializer.rb
# frozen_string_literal: true
class ApplicationSerializer < Panko::Serializer
end
active_interaction 用のファイルの作成
app/services/application_service.rb
# frozen_string_literal: true
class ApplicationService < ActiveInteraction::Base
end
日本語化用のファイルを追加します。
config/locales/services/defaults/ja.yml
ja.ymlの中身を貼り付けてください
タイムゾーンの設定
config/application.rb
module App
class Application < Rails::Application
...
config.api_only = true
+
+ config.time_zone = 'Tokyo'
end
end
ロケール設定追加
config/initializers/locale.rb
# frozen_string_literal: true
I18n.enforce_available_locales = true
Rails.application.config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.{rb,yml}').to_s]
Rails.application.config.i18n.available_locales = %i[ja en]
Rails.application.config.i18n.default_locale = :ja
日本語化用のファイルを追加します。
config/locales/ja.yml
ja.ymlの中身を貼り付けてください
不要であれば消しておきます。
$ rm config/locales/en.yml
Anyway Config の設定追加
$ make rails g anyway:install
アプリケーション全体の設定
config/configs/app_config.rb
# frozen_string_literal: true
class AppConfig < ApplicationConfig
attr_config :application_name,
:secret_key_base,
:default_url_host,
:default_url_port
end
config/app.yml
default: &default
application_name: 'Demo'
development:
<<: *default
test:
<<: *default
production:
<<: *default
データベースの設定
config/configs/db_config.rb
# frozen_string_literal: true
class DbConfig < ApplicationConfig
attr_config :database_name,
:database_host,
:database_user,
:database_password,
:database_socket,
:database_pool
end
config/db.yml
default: &default
database_name: 'demo'
database_socket: '/tmp/mysql.sock'
database_pool: 5
development:
<<: *default
test:
<<: *default
database_name: 'demo_test'
production:
<<: *default
合わせて config/database.yml と config/application.rb も修正します。
config/database.yml
default: &default
adapter: mysql2
encoding: utf8mb4
charaset: utf8mb4
host: <%= DbConfig.database_host %>
database: <%= DbConfig.database_name %>
username: <%= DbConfig.database_user %>
password: <%= DbConfig.database_password %>
socket: <%= DbConfig.database_socket %>
pool: <%= DbConfig.database_pool %>
development:
<<: *default
test:
<<: *default
production:
<<: *default
config/application.rb
module App
class Application < Rails::Application
...
config.time_zone = 'Tokyo'
+
+ Rails.application.routes.default_url_options[:host] = AppConfig.default_url_host
+ Rails.application.routes.default_url_options[:port] = AppConfig.default_url_port
end
end
RSpec の設定追加
$ make rails generate rspec:install
spec/support
配下のファイルがロードされるようにします。
spec/rails_helper.rb
-# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
+Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
factory_bot の設定追加
spec/support/factory_bot.rb
# frozen_string_literal: true
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.before do
FactoryBot.reload
end
end
ジェネレーターの設定変更
config/initializers/generators.rb
# frozen_string_literal: true
Rails.application.config.generators do |g|
g.test_framework :rspec, fixtures: true
g.fixture_replacement :factory_bot, dir: 'spec/factories'
end
annotate の設定追加
$ make rails g annotate:install
lib 配下のファイルをロードするようにします。
config/application.rb
module App
class Application < Rails::Application
...
config.time_zone = 'Tokyo'
+ config.autoload_paths << Rails.root.join('lib')
end
end
Bullet の設定追加
$ make rails g bullet:install
Enabled bullet in config/environments/development.rb
Would you like to enable bullet in test environment? (y/n) n
Enumerize の設定追加
config/locales/enumerize.ja.yml
ja:
enumerize:
API 関連の設定
CORS の設定追加
config/initializers/cors.rb
-# Rails.application.config.middleware.insert_before 0, Rack::Cors do
-# allow do
-# origins "example.com"
-#
-# resource "*",
-# headers: :any,
-# methods: [:get, :post, :put, :patch, :delete, :options, :head]
-# end
-# end
+Rails.application.config.middleware.insert_before 0, Rack::Cors do
+ allow do
+ # TODO: ドメインが決まったら設定すること
+ # origins 'example.com'
+
+ resource '*',
+ headers: :any,
+ methods: %i[get post put patch delete options head]
+ end
+end
rswag の設定追加
$ make rails g rswag:install
spec/swagger_helper.rb
# frozen_string_literal: true
require 'rails_helper'
require 'rails_helper'
+require 'json/schema_builder'
+
+JSON::SchemaBuilder.configure do |opts|
+ opts.validate_schema = true
+ opts.strict = true
+end
+
+Dir[Rails.root.join('spec/schemas/**/*.rb')].each { |file| require file }
RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
# to ensure that it's configured to serve Swagger from the same folder
config.swagger_root = Rails.root.join('swagger').to_s
# Define one or more Swagger documents and provide global metadata for each one
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
# be generated at the provided relative path under swagger_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
servers: [
{
url: 'http://{defaultHost}',
variables: {
defaultHost: {
- default: 'www.example.com'
+ default: 'localhost:3000'
}
}
}
]
}
}
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
# The swagger_docs configuration option has the filename including format in
# the key, this may want to be changed to avoid putting yaml in json files.
# Defaults to json. Accepts ':json' and ':yaml'.
config.swagger_format = :yaml
end
$ mkdir -p {'spec/schemas','spec/requests','spec/services'}
$ touch {'spec/schemas/.keep','spec/requests/.keep','spec/services/.keep'}
Seed Fu の設定追加
db/seeds.rb
SeedFu.seed
$ mkdir -p db/fixtures/
$ touch db/fixtures/.keep
Credentials の設定
$ make rails secret
$ make credential.edit development
app:
secret_key_base: 'make rails secret で出力された値を設定'
$ make rails secret
$ make credential.edit test
app:
secret_key_base: 'make rails secret で出力された値を設定'
$ make rails secret
$ make credential.edit production
app:
secret_key_base: 'make rails secret で出力された値を設定'
db:
database_host: ''
database_user: ''
database_password: ''
GitHub Actions の設定
.github/workflows/ci.yml
name: CI
on:
pull_request:
branches:
- main
- feature/*
jobs:
ci:
name: 'CI'
runs-on: ${{ matrix.os }}
env:
RAILS_ENV: test
APP_DEFAULT_URL_HOST: localhost
APP_DEFAULT_URL_PORT: 3000
DB_DATABASE_HOST: 127.0.0.1
DB_DATABASE_USER: root
DB_DATABASE_PASSWORD: root
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
strategy:
matrix:
os: [ubuntu-latest]
ruby-version: ['3.2.2']
steps:
- name: Checkout 🛎
uses: actions/checkout@master
- name: Set up Ruby ✨
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Cache gems 📦
uses: actions/cache@preview
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Start MySQL 🚀
run: sudo /etc/init.d/mysql start
- name: Setup Project 👔
run: bin/rails db:prepare
- name: Install dependencies 💻
run: |
gem install bundler --no-document
bundle config set --local path 'vendor/bundle'
bundle install --jobs 4
- name: Run rubocop 🤖
run: bundle exec rubocop
- name: Run brakeman ⚠
run: bundle exec brakeman
- name: Run tests 🧪
run: bundle exec rspec ./spec
VSCode の設定
.vscode/extensions.json
{
"recommendations": [
"aki77.rails-db-schema",
"bung87.rails",
"castwide.solargraph",
"GitHub.copilot",
"Hridoy.rails-snippets",
"KoichiSasada.vscode-rdbg",
"LoranKloeze.ruby-rubocop-revived",
"noku.rails-run-spec-vscode",
"rebornix.ruby"
],
"unwantedRecommendations": []
}
.vscode/settings.json
{
"[ruby]": {
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "LoranKloeze.ruby-rubocop-revived"
},
"ruby.intellisense": "rubyLocate",
"ruby.useLanguageServer": false,
"ruby.useBundler": false,
"ruby.format": "rubocop",
"ruby.lint": {
"rubocop": {
"forceExclusion": true
}
},
"ruby.rubocop.useBundler": false,
"solargraph.useBundler": false
}
DB の作成
$ make db.create
API Doc ファイルの生成
$ make swagger.create
不要なコードの削除
Dockerfile
-RUN bundle install # rails new が終わったら削除する
動作確認
Discussion