Docker環境のRuby on RailsアプリをCircleCIで自動ビルド＆テスト(RSpec)する方法


# はじめに

こんにちは、Masuyama です。

私は [CircleCI のユーザコミュニティ](https://circleci.connpass.com/)の運営に参画させていただいているので
本記事では、以下のような環境で 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

各ファイルの中身は下記の通りです。

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

```docker-compose.yml
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:
```

```ruby:Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6'
```

```Gemfile.lock(空ファイル)

```

```entrypoint.sh
#!/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** の一部を直接修正します。

```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** にアクセスして確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/a8501ddd-b082-778c-f06b-e8be449272bb.png)

Rails でお馴染みのこの画面が表示されたので、まずは無事に Docker 環境 Rails アプリの準備ができました。

# 自動テスト(RSpec)の準備

## gem のインストール

まずは Gemfile に **rspec-rails** を追加します。
本番環境では RSpec を使わないので、開発とテスト環境にのみ gem をインストールさせます。

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

上記コマンドに以下のマイグレーションファイルが自動されます。

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

上記コマンドにより以下のファイルが自動生成されます。

```ruby: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 を持ったユーザーを登録できることを確認します。

```ruby: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](https://circleci.com/ja/vcs-authorize/) の管理ページに GitHub でログインします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/b6c551d3-203e-1acf-fa84-bdcc7aabdc82.png" width=50%>

Projects の一覧に先ほど作成したリポジトリが同期されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/15c5609c-13ac-7a91-c13f-9e808b89b453.png)

確認できたら、次は CircleCI で自動テストを行うためのファイルを準備します。

プロジェクト直下に **.circleci** というフォルダ、そしてその下に **config.yml** というファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/b758eb28-fee7-e66b-def7-f8038c897757.png)

これを他のソースコードと一緒に GitHub にプッシュすることで CircleCI が走ることになります。

config.yml をローカルで作成したら、CirclecI 上で Prject 画面から Use Existing Config を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/76a8792c-9d56-7a41-9592-44aab4297437.png)

Start Building を実行します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/6ba9b418-5bd3-b101-5a27-01e76c545a0a.png" width=50%>

.circleci/config.yml の中身は次のように記述します。

```yaml:.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 のダッシュボードから状況を確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/237cdb0a-e710-0290-8095-c6c36fa8dda1.png" width=80%>

CircleCI 上でテスト環境の構築ビルドとテストに成功するとこのような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/895d011a-cf43-54f1-8d7b-32cd45f1c84e.png)

**test** を選択してみてみると、ローカル環境で試した時と同じように RSpec が実行され
失敗なし (failure 0) という結果が返ってきていることを確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/3747baa3-759b-1116-caef-cb0f9fc9709f.png" width=70%>
ここで失敗していると CircleCI 上の Jobs が fail になるため、問題のあるコードは続きのデプロイには進まないことになります。
開発者としてはテスト結果を待つことなく次のアクションが自動で決まるのは便利ですね。

# おわりに

CircleCI のような CI/CD 構成をゼロから構築するのはハードルが高いかと思いますが、
この記事を参考にすればコピペだけで Ruby on Rails をコンテナ環境で CI/CD を実現することが出来ます。

広く、一般的な技術になりつつある CI/CD をこの機会に試してみてはいかがでしょうか。