4
Help us understand the problem. What are the problem?

posted at

updated at

Organization

Ruby on Railsプロジェクトの開発環境をDocker化する

こちらの16日目の記事です。

概要

現在運用中のRuby on Railsプロジェクトの開発環境をDocker化する案件があり、その際に行った移行作業の手順を示します。

Dockerについて

Dockerとは、コンテナと呼ばれる仮想環境を構築・実行できるようにするためのプラットフォームです。

私は今回初めてDockerを触ったのですが、Dockerの理解にあたっては入門Dockerが大変参考になりました。

前提

バージョン

  • macOS Catalina 10.15.7
  • Docker 19.03.13
  • docker-compose 1.27.4
  • Ruby 2.4.5
  • mongoDB 3.0.15
  • postgres 10

システム構成

こちらのシステム構成図の通りにDocker化していきます。
(※大分簡略化しています)

system.png

初めにRailsアプリケーションのDocker Containerを作成し、
その後オーケストレーションツールであるdocker-composeによって、
RailsアプリケーションをmongoDB及びpostgreSQLに接続します。

手順

Dockerのインストール

Docker HubよりDocker Desktop for Macを導入します。

ターミナルで下記の2つのコマンドが実行できればOKです。

$ docker -v
Docker version 19.03.13, build 4484c46d9d
$ docker-compose -v
docker-compose version 1.27.4, build 40524192

必要なファイルの作成

Docker及びdocker-composeを動かすのに必要なファイルをプロジェクトのルートに作成します。

Dockerfile

ruby 2.4.5環境が予めインストールされているruby:2.4.5-slimというDockerイメージをDocker Hubから取得し、そのイメージ上にRails環境をセットアップしています。

Dockerfile

FROM ruby:2.4.5-slim

# Dockerコンテナ上におけるプロジェクトルートを指定
ENV APP_ROOT=/app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

# apt-utilsインストールの時の警告を抑制する
# https://qiita.com/haessal/items/0a83fe9fa1ac00ed5ee9
ENV DEBCONF_NOWARNINGS yes

# aptパッケージのインストール
RUN apt-get update -y -qq && \
    apt-get install -y -qq build-essential libpq-dev libmagickwand-dev

# Railsのセットアップ
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN gem install bundler -v 1.17.3 && bundle install

# プロジェクトディレクトリをDocker Imageにコピー
COPY . $APP_ROOT

docker-compose.yml

postgres, mongo, webの3つのサービスを定義し、
webpostgres及びmongoに依存させています。

docker-compose.yml
version: "3"

services:
    # postgreSQL containerの定義
    postgres: 
        image: postgres:10
        ports:
            # <Host Port>:<Container Port>
            - "5432:5432"
        environment:
            POSTGRES_USER: xxxxxx
            POSTGRES_PASSWORD: xxxxxx

    # mongoDB containerの定義
    mongo:
        image: mongo:3.0.15
        ports:
           - "27017:27017"

    # Rails app containerの定義
    web: 
        build: . 
        env_file: .env
        # pid error の回避のため、server.pidを削除したのちにrails sを実行
        # https://qiita.com/sakuraniumarete/items/ac07d9d56c876601748c
        command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
        # 依存関係の定義 (webをビルドするとpostgresとmongoが同時にビルドされる)
        depends_on:
            - postgres
            - mongo

ビルド実行

これら2つのファイルを作成すると、

$ docker-compose build

でコンテナをビルドできるようになります。

DBの永続化

現在の状態ではDBがコンテナ内部のストレージに生成されており、
コンテナを削除して再ビルドすると、DBに保存されていたデータは全て消失してしまいます。

DB上のデータを永続化するためには、Dockerが提供しているvolumeという機能を利用します。
volumeは、Docker Containerのライフサイクルからは独立して生成されるデータ保存領域です。
volume上にDBを生成することにより、コンテナを再ビルドしてもDB上のデータが残り続けるようになります。

image.png
引用元: https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/

volumeを利用するためには、docker-compose.ymlに以下の内容を追記します。

docker-compose.yml
 services:
    postgres: 
+        volumes:
+            - "postgres-data:/var/lib/postgresql/data"
    mongo:
+        volumes:
+            - "mongo-data:/data/db"
+ volumes:
+    postgres-data:
+    mongo-data:

上記を追記した上で改めてビルドすると、Docker Volumeが作成されているはずです。

$ docker volume ls
local               mongo-data
local               postgres-data

ホストとコンテナのソースコードを同期

現在の状態では、Dockerfileをビルドした時点で、ホストの全データををImageにコピーしています。

Dockerfile
COPY . $APP_ROOT

つまり、これより後にホスト側でソースコードを変更した場合、動作しているコンテナを一旦停止させ、
docker-compose build からやり直す必要があります。

開発環境において毎度ビルドからやり直しているのでは非常に効率が悪いので、
ホストのコード変更をコンテナに即時反映できるようにします。

よくある方法は、下記のように、プロジェクトのルートディレクトリを無名volumeとしてコンテナにマウントする方法です。

docker-compose.yml
services:
    # ${APP_ROOT}はDockerfileにおいてENVで定義した環境変数
    web:.:${APP_ROOT}

しかし上記の方法では、Docker for Mac特有のVolume I/Oの遅さがパフォーマンスへ影響を及ぼすという問題があります。
(この問題について、docker/for-macのGitHubリポジトリにおいて議論されています。)
手元の環境においては特にRSpecへの影響が顕著で、上記の方法でマウントした場合、普段5分ほどで完了していたテストが30分ほどかかりました...

docker-sync

この問題の解決策として、docker-syncというサードパーティライブラリを利用することができます。

新たにdocker-sync.ymldocker-compose-dev.ymlを作成します。
作成にあたってはdocker-syncのドキュメントを参考にしました。

docker-sync.yml
version: "2"

syncs:
  sync-volume:
    src: "."
    sync_excludes:
      - "log"
      - "tmp"
      - ".git"

docker-compose-dev.yml

version: "3"

services:
  web:
    volumes:
      - "sync-volume:/app:nocopy"

volumes:
  sync-volume:
    external: true

DBセットアップ

以下のコマンドで、postgreSQLとmongoDBをセットアップします。

$ docker-compose run --rm -e RAILS_ENV=development -T web rake db:setup
$ docker-compose run --rm -e RAILS_ENV=devlopment -T web rake db:mongoid:create_indexes

docker-compose run --rm <container name> <command>は、
指定したコンテナサービスを起動し、任意のコマンドを実行後、そのコンテナを削除するというコマンドです。
DBはvolumeで永続化されているので、セットアップのためだけにコンテナを作成し、その後削除してしまっても問題ないということです。

Railsサーバーの実行

ここまでの手順を実施した上で、下記コマンドを実行することでRailsサーバーが起動します。

$ docker-sync-stack start

これは以下のコマンドを短縮したものです。

$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。
参考: 設定の追加と上書き - Docker-docs-ja

また、上記はフォアグラウンドで起動するためのコマンドで、
バックグラウンドで起動する場合は以下のコマンドを実行します。

# 起動
$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up
# 停止
$ docker-compose down
$ docker-sync stop

テスト実行

RSpecを実行するためには、サーバーを起動した状態で以下のコマンドを実行します。

$ docker-compose exec -e COVERAGE=true -T web bundle exec rspec

docker-compose execで、起動中のDockerコンテナに対して任意のコマンドを実行できます。

もしくは、以下のようにコンテナに入って実行することもできます。

$ docker-compose exec web bash
root@container:/app# bundle exec rspec

CI対応

CIツールとしてJenkinsを使用しています。

テストを実行するシェルスクリプト

ビルドジョブにおいて、下記のシェルを実行することで自動テストが行われるようにしました。

# 終了時の処理
# docker-composeが失敗した際でもJenkinsビルドマシンにゴミが残らないよう後処理をかける
# https://qiita.com/ryo0301/items/7bf1eaf00b037c38e2ea
function finally {
    # Clean project
    docker-compose down --rmi local --volumes --remove-orphans
}
trap finally EXIT

# 並列実行のために、プロジェクト名としてJenkinsの環境変数である$BUILD_TAGを指定
export COMPOSE_PROJECT_NAME=$BUILD_TAG

# 環境変数で予めビルドするdocker-composeファイルを指定することで、
# -fオプションによる指定を省略できる
# https://docs.docker.jp/compose/reference/envvars.html
export COMPOSE_PATH_SEPARATOR=:
export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

# Build Container
docker-compose build --no-cache
docker-compose up -d

# Setup DB
docker-compose exec -e RAILS_ENV=test -T web rake db:setup
docker-compose exec -e RAILS_ENV=test -T web rake db:mongoid:create_indexes

# Run RSpec
docker-compose exec  -e COVERAGE=true -T web bundle exec rspec

ビルドジョブを並列実行できるようにする

RailsプロジェクトをDocker化していない時の問題として、
2つ以上のビルドジョブを並列実行すると、同じマシン上でDBの取り合いが起こり、
エラーが発生する問題がありました。

Docker化したことで、それぞれのビルドが独立したコンテナの中で実行されるようになり、
並列実行してもエラーが起こらないようになります。
ただし、並列ビルドの実行時にコンテナのポート番号が重複しないよう、
ポートフォワーディングの設定を変更する必要があります。
参考: ホスト上にコンテナのポートを割り当て - Docker-docs-ja

export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

で指定している docker-compose-test.ymlの中身でそれを行っています。

docker-compose-test.yml
version: "3"

services:
    postgres:
        ports:
            - "5432"
    mongo:
        ports:
            - "27017"
    web:
        ports:
            - "3000"

また、docker-compose.ymlに記載したポート番号をdocker-compose-dev.ymlに移動する必要があります。

docker-compose.yml
services:
    postgres:
-        ports:
-            - "5432:5432"
    mongo:
-        ports:
-            - "27017:27017"
    web:
-        ports:
-            - "3000:3000"
docker-compose-dev.yml
services:
    postgres:
+        ports:
+            - "5432:5432"
    mongo:
+        ports:
+            - "27017:27017"
    web:
+        ports:
+            - "3000:3000"

理由としては、このまま docker-compose up -d を実行すると、docker-compose.ymldocker-compose-test.ymlがマージされ、
結果としてポートの指定が以下のようになってしまい、docker-compose-test.ymlでわざわざポート指定した意味がなくなってしまうためです。

services:
    postgres:
        ports:
            - "5432:5432"
            - "5432"
    mongo:
        ports:
            - "27017:27017"
            - "27017"
    web:
        ports:
            - "3000:3000"
            - "3000"

Railsサーバーの実行の項で、

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。

と述べましたが、複数指定可能なパラメータの場合は設定値は上書きされずにマージされるので注意が必要です。

おまけ: RubyMineへの対応

JetBrainsのIDEであるRubyMineは、Docker Container上のRuby on Railsの開発環境に完全対応しており、以下の手順で設定することができます。
チュートリアル : リモートインタープリターとしての Docker Compose — RubyMine

まとめ

今回新たに作成したファイル

開発環境をDocker化するにあたり、新たに作成したファイルは以下の通りです。

.
├── Dockerfile
├── docker-compose.yml
├── docker-compose-dev.yml
├── docker-compose-test.yml
└── docker-sync.yml

その他

今回初めてコンテナ技術に触れ、Docker化にあたっては様々な試行錯誤を重ねました。
これまでに書いた中で、もっと良い対応方法がある、或いは対応方法として正しくない箇所があるかもしれませんが、その時はご指摘いただければ幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?