Docker環境のRuby on RailsアプリをCircleCIで自動ビルド&テスト(RSpec)する方法
はじめに
こんにちは、Masuyama です。
私は CircleCIのユーザコミュニティの運営に参画させていただいているので
本記事では、以下のような環境でRuby on Railsアプリの自動ビルド&テストを実現してみたいと思います。
- コンテナ環境:Docker (docker-compose)
- フレームワーク:Ruby on Rails
- テスト方法:RSpec
- CI/CDツール:CircleCI
CircleCI、Docker を使ったことが無い方でも理解できるように解説しているつもりですが
質問がありましたら気軽にコメントをいただければと思います。
CircleCIとは?
概要
CircleCIはCI/CDをクラウド上で行えるツールであり、ビルド・テスト・デプロイの3つを自動で行なえます。
どのような環境でビルド・テスト・デプロイを行うかは config.yml というファイルを作成して指定します。
プロジェクト直下に .circleci というフォルダを作り、その下に config.yml というファイルを作っておくと
Github にコードをアップロードした時に自動的に CircleCI が読み取り、各アクションを実行してくれます。
CircleCIで出来ること
ビルド
アプリケーションが動作する環境を含め、アプリケーションを構築します。
今回のように Docker 環境だと Docker イメージのプル、各モジュールのインストールなどを行います。
テスト
事前に指定しておいたテストを実行します。
今回は Rails の RSpec でテストを実行しますが、テスト結果に失敗(failure)が含まれていなければデプロイまで進むことになります。
デプロイ
今回はデプロイまでは行いませんが、テストまでパスしたコードを本番環境へ反映させることができます。
Docker環境構築
まずはDockerでRailsのサーバを立ち上げるところまで準備します。
環境情報
- Ruby: 2.7.2
- Rails: 6.0.3.4
- データベース:Postgres
設定ファイル作成
適当なフォルダを作成し、その下に以下のファイルを作成していきます。
- Dockerfile
- docker-compose.yml
- Gemfile
- Gemfile.lock (空ファイル)
- entrypoint.sh
各ファイルの中身は下記の通りです。
FROM ruby:2.7.2-alpine
ENV LANG=ja_JP.UTF-8
ENV TZ=Asia/Tokyo
ENV ROOT=/myapp \
GEM_HOME=/bundle \
BUNDLE_PATH=$GEM_HOME
ENV BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH
WORKDIR $ROOT
RUN apk update && \
apk upgrade && \
apk add --no-cache \
gcc \
g++ \
libc-dev \
libxml2-dev \
linux-headers \
make \
nodejs \
postgresql \
postgresql-dev \
tzdata \
imagemagick \
yarn && \
apk add --virtual build-packs --no-cache \
build-base \
curl-dev
COPY Gemfile $ROOT
COPY Gemfile.lock $ROOT
RUN bundle install -j4
# 不要ファイル削除
RUN rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \
apk del build-packs
COPY . $ROOT
# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["sh", "/usr/bin/entrypoint.sh"]
EXPOSE 3000
version: "3.8"
services:
db:
image: postgres:11.0-alpine
volumes:
- postgres:/var/lib/postgresql/data:cached
ports:
- "5432:5432"
environment:
PGDATA: /var/lib/postgresql/data/pgdata
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
TZ: Asia/Tokyo
app:
build: .
command: ash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/myapp:cached
- rails_cache:/myapp/tmp/cache
- node_modules:/myapp/node_modules:cached
- bundle:/bundle:cached
tmpfs:
- /tmp
tty: true
stdin_open: true
ports:
- "3000:3000"
environment:
RAILS_ENV: development
NODE_ENV: development
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_USER: postgres
DATABASE_PASSWORD: password
WEBPACKER_DEV_SERVER_HOST: webpacker
depends_on:
- db
- webpacker
webpacker:
build: .
command: ./bin/webpack-dev-server
volumes:
- .:/myapp:cached
- node_modules:/myapp/node_modules:cached
environment:
RAILS_ENV: development
NODE_ENV: development
WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
tty: false
stdin_open: false
ports:
- "3035:3035"
volumes:
rails_cache:
node_modules:
postgres:
bundle:
Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6'
# !/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
コンテナ作成
まずは rails new コマンドでアプリを生成します。
docker-compose run app rails new . --force --no-deps --database=postgresql --skip-bundle
実行後、一気に Rails 関連ファイルが作成されます。
この時点で Gemfile が更新されているため、bundle update を実行しておきます。
※bundle updateを実行すると、Gemfile を元に gem のインストールが行われ、Gemfile.lock も更新されます。
docker-compose run app bundle update
この後、config/database.yml の一部を直接修正します。
default: &default
adapter: postgresql
encoding: unicode
host: db # 追加
username: postgres # 追加
password: password # 追加
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
...
あとは以下のコマンドを叩きます。
docker-compose build --no-cache
docker-compose run app rails webpacker:install
docker-compose run app rails db:create
docker-compose run app yarn install --check-files
docker-compose run app yarn add jquery bootstrap popper.js
なお、今回の目的を達成するだけであれば yarn や jquery 等は不要ですが、
アプリ作成の上ではこれらが必要になることが多いため、汎用的に使えるようにインストールしています。
ここまで終わったらコンテナを起動します。
docker-compose up -d
数秒ほど待っていただいたら、ブラウザで localhost:3000 にアクセスして確認しましょう。
Rails でお馴染みのこの画面が表示されたので、まずは無事に Docker 環境 Rails アプリの準備ができました。
自動テスト(RSpec)の準備
gem のインストール
まずは Gemfile に rspec-rails を追加します。
本番環境では RSpec を使わないので、開発とテスト環境にのみ gem をインストールさせます。
Gemfile
...
group :development, :test do
...
gem 'rspec-rails' # 追加
end
Gemfile を書き換えたので、イメージをビルドし直します。
docker-compose build
RSpec の基本設定
以下のコマンドで RSpec をインストールしましょう。
docker-compose run app rails g rspec:install
RSpecの実行
この段階ではまだ何もテストは書いていませんが、動作確認の意味で RSpec を実行してみます。
$ docker-compose run app rails spec
Starting rails-scaffold-circleci_webpacker_1 ... done
Creating rails-scaffold-circleci_app_run ... done
No examples found.
Finished in 0.00124 seconds (files took 0.38309 seconds to load)
0 examples, 0 failures
まだ何もテストを書いていないので当然テスト(exmaple) は 0、失敗(failure)も 0 ですが
とりあえず RSpec が動作していることは確認できました。
テスト用 model 生成
最低限のテストを行うため、適当に User モデルだけ作成しておきます。
string型の name, email カラムを用意しておきましょう。
docker-compose run app rails g model User name:string email:string
上記コマンドに以下のマイグレーションファイルが自動されます。
db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
自動生成されたマイグレーションファイルをもとに、マイグレーションを行います。
docker-compose run app rails db:migrate
これで、name カラムと email カラムを持った User モデルが作成されました。
次は User モデルに対するテストを RSpec で書いてみます。
Userモデルのテスト用RSpecファイルを生成
rspec-rails を入れているので、テスト用のファイルは rails g コマンドで生成できます。
docker-compose run app rails g rspec:model User
上記コマンドにより以下のファイルが自動生成されます。
spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
これを書き換えていきましょう。
今回はとりあえずテストが出来ればよいので、適当な name と email を持ったユーザーを登録できることを確認します。
spec/models/user_spec.rb(修正後)
require 'rails_helper'
RSpec.describe User, type: :model do
it "name と email を持ったユーザーを登録できること" do
user = User.new(
name: "John",
email: "john@example.com",
)
# オブジェクトをexpectに渡す動作が有効であることを確認
expect(user).to be_valid
end
end
これで RSpec を実行してみましょう。
docker-compose run app rails spec
失敗 (failure) は無く成功していることを確認します。
Finished in 0.10029 seconds (files took 8.17 seconds to load)
1 example, 0 failures
これでテストが実行できることを確認しました。
CircleCI 構築
GitHubの準備
今回、GitHub にソースコードをプッシュしたタイミングで CircleCI が走るようにしたいので、GitHub のリポジトリを準備していきます。
新規リポジトリを作成
advent-2020 というリポジトリを作成し、こちらに push していきます。
リポジトリ作成直後に表示されている指示に従えば OK です。
CircleCI の準備
まだ使ったことが無い人は CircleCI の管理ページに GitHub でログインします。
Projects の一覧に先ほど作成したリポジトリが同期されていることを確認します。
確認できたら、次は CircleCI で自動テストを行うためのファイルを準備します。
プロジェクト直下に .circleci というフォルダ、そしてその下に config.yml というファイルを作成します。
これを他のソースコードと一緒に GitHub にプッシュすることで CircleCI が走ることになります。
config.yml をローカルで作成したら、CirclecI 上で Prject 画面から Use Existing Config を選択します。
Start Building を実行します。
.circleci/config.yml の中身は次のように記述します。
.circleci/config.yml
version: 2.1
orbs:
ruby: circleci/ruby@1.1.2
node: circleci/node@2
# setup
commands:
setup:
steps:
- checkout
- ruby/install-deps
- node/install-packages:
pkg-manager: yarn
cache-key: "yarn.lock"
jobs:
build:
docker:
- image: circleci/ruby:2.7-node
steps:
- setup
test:
docker:
- image: circleci/ruby:2.7-node
- image: circleci/postgres:11.6-alpine
name: "db"
environment:
POSTGRES_USER: postgres
POSTGRES_DB: myapp_test
POSTGRES_PASSWORD: ""
environment:
BUNDLE_JOBS: "4"
BUNDLE_RETRY: "3"
PGHOST: 127.0.0.1
PGUSER: postgres
PGPASSWORD: ""
RAILS_ENV: test
steps:
- setup
- run:
name: rails db:migrate
command: bundle exec rails db:migrate
- run:
name: Database setup
command: bundle exec rails db:schema:load --trace
- run:
name: Rspec
command: bundle exec rspec
workflows:
version: 2
test_and_deploy:
jobs:
- build
- test:
requires:
- build
これを含めたプロジェクト内のソースコードを GitHub にプッシュします。
git add .
git commit -m "CircleCI準備完了"
git push -u origin main
push すると自動で CircleCI に連携され、ビルドがまず始まり、ビルドが完了するとテストも自動で始まります。
進行状況は CircleCI のダッシュボードから状況を確認できます。
CircleCI 上でテスト環境の構築ビルドとテストに成功するとこのような表示になります。
test を選択してみてみると、ローカル環境で試した時と同じように RSpec が実行され
失敗なし (failure 0) という結果が返ってきていることを確認できます。
ここで失敗していると CircleCI 上の Jobs が fail になるため、問題のあるコードは続きのデプロイには進まないことになります。
開発者としてはテスト結果を待つことなく次のアクションが自動で決まるのは便利ですね。
おわりに
CircleCI のような CI/CD 構成をゼロから構築するのはハードルが高いかと思いますが、
この記事を参考にすればコピペだけで Ruby on Rails をコンテナ環境で CI/CD を実現することが出来ます。
広く、一般的な技術になりつつある CI/CD をこの機会に試してみてはいかがでしょうか。
Discussion