こちらの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化していきます。
(※大分簡略化しています)
初めに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環境をセットアップしています。
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つのサービスを定義し、
web
をpostgres
及びmongo
に依存させています。
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上のデータが残り続けるようになります。
引用元: https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/
volumeを利用するためには、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にコピーしています。
COPY . $APP_ROOT
つまり、これより後にホスト側でソースコードを変更した場合、動作しているコンテナを一旦停止させ、
docker-compose build
からやり直す必要があります。
開発環境において毎度ビルドからやり直しているのでは非常に効率が悪いので、
ホストのコード変更をコンテナに即時反映できるようにします。
よくある方法は、下記のように、プロジェクトのルートディレクトリを無名volumeとしてコンテナにマウントする方法です。
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.yml
とdocker-compose-dev.yml
を作成します。
作成にあたってはdocker-syncのドキュメントを参考にしました。
version: "2"
syncs:
sync-volume:
src: "."
sync_excludes:
- "log"
- "tmp"
- ".git"
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
の中身でそれを行っています。
version: "3"
services:
postgres:
ports:
- "5432"
mongo:
ports:
- "27017"
web:
ports:
- "3000"
また、docker-compose.yml
に記載したポート番号をdocker-compose-dev.yml
に移動する必要があります。
services:
postgres:
- ports:
- - "5432:5432"
mongo:
- ports:
- - "27017:27017"
web:
- ports:
- - "3000:3000"
services:
postgres:
+ ports:
+ - "5432:5432"
mongo:
+ ports:
+ - "27017:27017"
web:
+ ports:
+ - "3000:3000"
理由としては、このまま docker-compose up -d
を実行すると、docker-compose.yml
とdocker-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化にあたっては様々な試行錯誤を重ねました。
これまでに書いた中で、もっと良い対応方法がある、或いは対応方法として正しくない箇所があるかもしれませんが、その時はご指摘いただければ幸いです。