Table of Contents
RailsアプリケーションをKubernetes(以後、k8s)で運用できるようにするための手順を書きます。 この記事はシリーズ連載記事の第二回です。
- 第一回 Docker編
- 第二回 Docker Compose/Dockerfile編
- 第三回 Kubernetes入門編
- 第四回 Kubernetes基礎編
- 第五回 Kubernetes応用編
- 第六回 Helm編
今回は下記について書きます。
- 最小限のDocker Compose入門
- Docker Composeを使った各種ミドルウェアのインストールと管理
- RailsアプリケーションのDockerイメージの作り方
- Docker Composeによるローカルプレビュー環境の構築
サンプルアプリケーション
簡単なRailsアプリを例に、Docker Composeの使い方やDockerfileの書き方を説明します。
このサンプルアプリrails-k8s-demoapp
のコードは下記のリポジトリに置いています。
https://github.com/kwhrtsk/rails-k8s-demoapp
rails-k8s-demoapp
は「フォームでメッセージを送信すると画面上のリストに追加して表示する」だけの小さなアプリです。
仕様は極小ですが、できるだけ一般的なRailsアプリの構成に近くなるように下記の要件を満たすものにしています。
- ミドルウェアとしてMySQLとRedis(SidekiqとActionCableのため)を使用
- ActiveJobを使用: Pumaの他にSidekiqプロセスの起動が必要なケースを想定
- 新しいメッセージが追加されると、3秒後に逆順の文字列をさらにメッセージとして追加するようなジョブをSidekiqで実行します。
- ActionCableを使用: websocketが必要なケースを想定
- 新しいメッセージが追加された時にActionCableでブラウザに通知を行いページリロードさせます。
- Webpackerを使用: アセットのビルドにnodeとyarnが必要なケースを想定
- フロントエンドのコードはTypeScriptで書いています。
- webpackでBootstrap v4を組み込んでいます。
下記のコマンドで動作を確認することができます。RubyやRailsの開発環境は必要ありません。 gitとDockerがインストールされていれば動きます。
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp
$ docker-compose -f docker-compose-preview.yml up -d
$ open http://localhost:3000/
docker-compose
コマンドの使い方やサンプルアプリケーションで使っているdocker-compose.yml
とDockerfile
については順に説明します。
最小限のDocker Compose入門
Docker Composeとは
複数のコンテナをYAML形式の構成ファイルで一括管理するツールです。
- この構成ファイルを Composeファイル と呼びます。
- 通常、ファイル名は
docker-compose.yml
です。
Railsアプリ開発において、Docker Composeには大きく二つの用途があります。
- 開発環境におけるMySQLやRedisなどのミドルウェアのインストールと実行管理
- 完成したRailsアプリを(特に開発者以外が)簡単に手元で実行するための仕組み
以降、それぞれの詳細について説明します。
Docker Composeを使った各種ミドルウェアのインストールと実行管理
一般的なRailsアプリケーションでは、PostgreSQLやMySQLなどのRDBMSに加えて、 しばしばRedisやElasticsearchなどのミドルウェアを使用します。 従来、macOS上でRailsアプリの開発を行う場合にはこれらのミドルウェアのmacOS版をインストールし、 サービスとして起動しておく必要があったため、READMEには長々とセットアップの手順を書いたりしていました。
一方、先に挙げたようなメジャーなプロダクトはいずれも公式のDockerイメージが存在するため、
適切に書かれた docker-compose.yml
ファイルさえあれば、
docker-compose up
コマンドを一つ実行するだけで必要なミドルウェアのイメージの取得からコンテナの起動まですべて自動で行うことができます。
また、コンテナとしてミドルウェアを起動する際には個別にバージョンやデータの保存場所を指定できるため、 導入の手順が簡単になるだけでなく下記のようなメリットもあります。
- 複数の開発プロジェクトを並行して進める際に、ミドルウェアの細かいバージョンや内部データを完全に独立して管理できる。
- 古いバージョンのミドルウェアを要求するようなプロジェクトの開発環境を維持しやすい。
Composeファイル(docker-compose.yml)のサンプル
前述のサンプルアプリケーションではMySQLとRedisを使います。
この2つを起動するためのdocker-compose.yml
は下記のような内容です。
通常、このファイルはプロジェクトのルートディレクトリに置きます。
version: "3"
services:
mysql:
image: mysql:5.7.21
environment:
- MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD
ports:
- 3306:3306
volumes:
- ./tmp/mysql:/var/lib/mysql
redis:
image: redis:4.0.9
ports:
- 6379:6379
volumes:
- ./tmp/redis:/data
command: redis-server --appendonly yes
services
の下にコンテナ(Docker Comoposeの文脈ではサービス)の定義を書いていきます。
mysql
については、前回Docker編のdocker container run(mysql編) に出てきた docker container run
コマンドとやっていることはほぼ同じです。
environment
: 環境変数を追加してコンテナを起動します。.envrc
に書かれている環境変数MYSQL_PASSWORD
をrootユーザのパスワードとして設定するため、MYSQL_ROOT_PASSWORD
の値として設定しています。- ローカルの開発環境で作業を行う際には、direnvなどのツールで
.envrc
の内容をシェルに設定するという想定です。 MYSQL_ALLOW_EMPTY_PASSWORD
にyes
を設定するとroot
のパスワードを空にすることもできます。
ports
: コンテナのポート(右側)をホスト側のポート(左側)にマッピングします。volumes
: ホスト側のパス(左側)をコンテナ上のパス(右側)にマッピングします。docker container run
の-v
オプションとは異なり、ホスト側の相対パスで指定できます。
redis
のcommand
には、データをストレージに永続化するためのオプションを指定しています。
Composeファイルには多数の機能があります。詳細については下記のリファレンスを参照してください。
mysql
とredis
のイメージに指定できる環境変数やコマンドのオプションについては、公式リポジトリのドキュメントを参照してください。
docker-compose up: 各種ミドルウェアのインストールと起動
まず、.envrc
に書かれた環境変数を設定してください。
$ source .envrc
# direnvを使っている場合は下記でも可
$ direnv allow
下記のコマンドを実行すると、Docker Hubからmysql
とredis
のイメージを取得してコンテナが起動します。
(-d
はバックグラウンドで起動するという意味です)
-f
で任意のComposeファイルを指定できますが、省略するとカレントディレクトリのdocker-compose.yml
が参照されます。
$ docker-compose up -d
Creating network "railsk8sdemoapp_default" with the default driver
Pulling mysql (mysql:5.7.21)...
5.7.21: Pulling from library/mysql
2a72cbf407d6: Pull complete
38680a9b47a8: Pull complete
4c732aa0eb1b: Pull complete
c5317a34eddd: Pull complete
f92be680366c: Pull complete
e8ecd8bec5ab: Pull complete
2a650284a6a8: Pull complete
5b5108d08c6d: Pull complete
beaff1261757: Pull complete
c1a55c6375b5: Pull complete
8181cde51c65: Pull complete
Digest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28c
Status: Downloaded newer image for mysql:5.7.21
Pulling redis (redis:4.0.9)...
4.0.9: Pulling from library/redis
b0568b191983: Pull complete
6637dc5b29fe: Pull complete
7b4314315f15: Pull complete
2fd86759b5ff: Pull complete
0f04862b5a3b: Pull complete
2db0056aa977: Pull complete
Digest: sha256:6b9f935e89af002225c0dcdadf1fd74245b4cc1e3e91222f7e4769c236cf80d4
Status: Downloaded newer image for redis:4.0.9
Creating railsk8sdemoapp_redis_1 ... done
Creating railsk8sdemoapp_mysql_1 ... done
2回目以降は取得済みのイメージを使用するため、起動は少し早くなります。
MySQLとRedisが起動した後であれば、下記のように開発環境用の各種プロセスを起動できます。
$ gem install foreman
$ bundle install --path=vendor/bundle
$ yarn install
$ ./bin/rails db:setup
$ foreman start -f Procfile
foremanでは、puma
、sidekiq
、webpack-dev-server
の三つを起動します。(ruby, node, yarnが必要です)
# Procfile
web: ./bin/rails s -p 3000
worker: ./bin/sidekiq
client: ./bin/webpack-dev-server
docker-compose ps: コンテナの一覧を取得する
docker-compose.yml
に定義されたサービスのコンテナ一覧を表示します。
$ docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------
railsk8sdemoapp_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp
railsk8sdemoapp_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:6379->6379/tcp
コンテナの名前は、${プレフィクス}_${サービス名}_${連番}
になります。
プレフィクスはカレントディレクトリから-
や_
を除いた文字列になります。
連番がついているのは一つのサービスに複数のコンテナが起動し得るためです。
(今回は紹介しませんが、docker-compose scale
コマンドでサービスごとのコンテナの数を増減させることができます)
docker-compose down: コンテナの削除
下記のコマンドを実行すると、docker-compose.yml
に書かれたすべてのサービスのコンテナを停止した後、削除します。
-v
オプションは関連するデータボリュームも削除するという意味です。(ホスト側のボリュームをマウントしている場合は残ります)
$ docker-compose down -v
Stopping railsk8sdemoapp_mysql_1 ... done
Stopping railsk8sdemoapp_redis_1 ... done
Removing railsk8sdemoapp_mysql_1 ... done
Removing railsk8sdemoapp_redis_1 ... done
Removing network railsk8sdemoapp_default
最小限のDockerfile入門
RailsアプリのDockerイメージを作る方法を説明します。
Dockerイメージを作るには、まずDockerfile
にイメージの作り方を記述します。
サンプルアプリ rails-k8s-demoapp に同梱している下記のようなDockerfile
を例に説明します。
### image for build
FROM ruby:2.5.1-alpine AS build-env
ARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata yaml"
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT
# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES
# install rubygem
COPY Gemfile Gemfile.lock $RAILS_ROOT/
RUN bundle install -j4 --path=vendor/bundle
# install npm
COPY package.json yarn.lock $RAILS_ROOT/
RUN yarn install
# build assets
COPY . $RAILS_ROOT
RUN bundle exec rake webpacker:compile
RUN bundle exec rake assets:precompile
### image for execution
FROM ruby:2.5.1-alpine
LABEL maintainer 'Kawahara Taisuke <[email protected]>'
ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata yaml mariadb-client-libs bash"
ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT
# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $PACKAGES
COPY --from=build-env $RAILS_ROOT $RAILS_ROOT
Dockerfile
では、上記の例のようにFROM
やRUN
などのコマンドを一行に一つ書きます。各コマンドの詳細は下記のリファレンスを参照してください。
docker image build
コマンドでイメージの作成を行うと、Dockerfile
に書かれたコマンドが上から順に実行されます。
このDockerfile
でrails-k8s-demoapp
のイメージをビルドするには下記のようにします。
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp
$ docker image build . -t demoapp:latest
.
はビルドコンテキストです。通常、Dockerfile
が置いてあるパスを指定します。
-t
はイメージの名前です。
- docker image build | Docker Documentation
- 旧コマンド名は
docker build
です。ドキュメントはこちらの方が詳しいです。
これでRailsアプリのイメージができました。下記のコマンドでコンテナを起動できます。
$ docker container run -it --rm demoapp:latest ls
Gemfile lib
Gemfile.lock log
Procfile node_modules
README.md package.json
Rakefile public
app spec
bin storage
config test
config.ru tmp
db tsconfig.json
docker-compose-preview.yml vendor
docker-compose.yml yarn.lock
k8s
この例ではls
コマンドを実行してコンテナのファイルを表示しています。
上記のようにアプリケーションルートディレクトリの中身が表示されるはずです。
次にDockerfile
の中身を順に解説していきます。
FROM
FROM
コマンドではベースイメージを指定します。サンプルのDockerfile
では2回出てきますが、このケースでは2つイメージを作っています。
FROM ruby:2.5.1-alpine AS build-env
# ...
FROM ruby:2.5.1-alpine
# ...
COPY --from=build-env $RAILS_ROOT $RAILS_ROOT
前半がgemやnpmのC拡張やjsやcssなどのアセットの ビルド用イメージ で、後半がアプリケーションとして運用する 実行用イメージ です。 後半では前半のビルドの結果を単にコピーしています。このように2段階に分けてイメージを作成している理由は、イメージのサイズを小さくするためです。 詳細は レイヤについて で説明します。
最終的なイメージの成果物は後者の 実行用イメージ です。 前者は削除しても構いませんが、残しておくと2回目以降のビルドが差分で実行されるため速くなります(これについても後述)。
次にベースイメージのruby:2.5.1-alpine
について説明します。
Docker Hubのrubyリポジトリには大まかに3系統のタグがあります。
ruby:<version>
: Debian stretchベースruby:slim
: Debian stretchベースだがインストールされたパッケージが少ないruby:alpine
: Alpine Linuxベース
今回はイメージサイズが最も小さいruby:2.5.1-alpine
を使います。
% docker images
ruby 2.5.1-alpine b620ae34414c 9 days ago 55.5MB
ruby 2.5.1-slim 85b814a932e6 9 days ago 172MB
ruby 2.5.1 1624ebb80e3e 9 days ago 863MB
また、Alpine Linuxのパッケージマネージャであるapk
はDebianのapt
と比べてNode.jsやYARNのインストールやキャッシュの制御がより簡単というメリットもあります。
ENV, ARG
ENV
とARG
はどちらもコンテナに環境変数を設定するコマンドですが、
ARG
で指定した環境変数はイメージのビルド時にだけ設定され、
作成済みのイメージをコンテナ化した際には残っていないという特徴があります。
また、docker image build
コマンドの--build-arg
やdocker-compose.yml
のargs
オプションで上書きすることができます。
例えば、プロキシ環境下でイメージをビルドする際にHTTP_PROXY
のような環境変数を指定する際には、ENV
ではなくARG
を使うのが望ましいです。
このサンプルではインストールするパッケージの名前などをARG
で設定しています。
また、ENV
では環境変数 RAILS_ENV
を production
に設定しています。
### image for build
# ...
# アプリケーションのインストール先
ARG RAILS_ROOT=/app
# gemやnpmのC拡張やjs, cssなどのアセットのビルドに必要なパッケージ
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata yaml"
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
### image for execution
# ...
# Railsアプリの実行に必要なパッケージ
ARG PACKAGES="tzdata yaml mariadb-client-libs bash"
ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
BUNDLE_APP_CONFIG
はrubyイメージがもともと持っている環境変数です。
このDockerfile
では、ビルド済みのbundleディレクトリを丸ごと実行イメージにコピーしたいので、
gemのインストール先を${RAILS_ROOT}/vendor/bundle
に指定しています。
このようにインストール先を変更した場合には BUNDLE_APP_CONFIG
を上記のように上書きしないとbundle exec
が正常に動作しません。
ただし、この挙動は紛らわしいという指摘もあるため、将来の更新で修正されるかもしれません。
WORKDIR
WORKDIR
はこのイメージから作成したコンテナ上でコマンドを実行するときのカレントディレクトリです。
この例では /app
をRailsアプリのルートディレクトリとして指定しているので、WORKDIR
も同じパスにしています。
ARG RAILS_ROOT=/app
WORKDIR $RAILS_ROOT
また、RUN
やCOPY
などのDockerfile
のコマンドもWORKDIR
で指定したパスで実行されます。
RUN, COPY
RUN
はコマンドを実行します。主にパッケージのインストールやアプリケーションのビルドなどを行います。
COPY
はホスト側のファイルをコンテナ側に複製します。
この時、.dockerignore
ファイルで指定されたファイルは複製されません。
パスワードやクレデンシャルのような秘匿値を書いたファイルは忘れずに.dockerignore
に追加してください。
また、.dockerignore
に一致したファイルはイメージのビルド時にdockerd
へ転送されなくなるため、
.git
やnode_modules
などビルド時に不要でかつサイズやファイル数が大きいディレクトリも指定するのがセオリーです。
今回ベースイメージにしているのはruby:2.5.1-alpine
というイメージですが、これはAlpine Linuxというディストリビューションをベースにしています。
Alpine Linuxではapk
というパッケージマネージャを使います。使用可能なパッケージを下記のサイトで検索できます。
Alpine Linux packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES
- RUN | Docker Documentation
- RUN | 日本語版 ドキュメント
- COPY | Docker Documentation
- COPY | 日本語版 ドキュメント
- .dockerignore | Docker Documentation
- .dockerignore | 日本語版 ドキュメント
レイヤについて
Dockerイメージのデータはレイヤと呼ばれる単位で記録されており、
Dockerfile
のRUN
やCOPY
などのコマンドは実行するたびに新しいレイヤに結果が記録されます。
またイメージのビルドや送受信はレイヤ単位でキャッシュされて差分実行されるため、
適切な単位でレイヤを分割しなければビルドやデプロイに無駄な時間がかかるようになります。
特に、一度追加したファイルは別のレイヤで削除したとしても以前のレイヤに残り続けるため、
イメージ全体のファイルサイズは減らない点に注意が必要です。
一般的に、Dockerfile
ではイメージのファイルサイズを減らしたりビルドやデプロイの速度を上げるために次のような工夫をします。
Gemfile
、Gemfile.lock
のコピーとbundle install
の実行はアプリケーションコードのCOPY
より前で個別に行う。
gemの追加は比較的頻度の少ない作業なので、こうしておくとbundle install
の頻度を減らしてビルドを高速化できます。
package.json
とyarn.lock
のコピー、yarn
の実行についても同様です。
- ビルド用のイメージと実行用のイメージを分けて、実行時に必要なファイルだけをビルド用のイメージから実行用のイメージにコピーする。
このようにすることでビルドに必要なパッケージが含まれるレイヤを丸ごと削除できます。
「1つのRUN
コマンドでパッケージのインストール、ビルド、パッケージやキャッシュの削除を行う」ことでも同じことを実現できますが、
パッケージのインストールとビルドのレイヤが同じになるので、アプリのコードを更新しただけでもビルド時にはパッケージのインストールからやり直しになり、余分に時間がかかるようになります。
COPY
コマンドの--from
オプションは比較的最近追加された機能なので、
古いドキュメントにはよくこのようなやり方が書いてあります
(古いランタイムに配慮して止むを得ずこのような実装になっているケースもあるかもしれません)。
docker-composeコマンドによるローカルプレビュー環境
RailsアプリのDockerイメージを作れるようになったので、 次は「Ruby/Railsの開発環境がなくても、DockerさえあればRailsアプリのイメージを作成して起動できるようにする」ための Composeファイルを用意します。
ローカルでの開発用に docker-compose.yml
がすでにあるので、docker-compose-preview.yml
というファイル名で用意します。
version: "3"
services:
puma:
image: demoapp
build:
context: .
env_file:
- .dockerenv/rails
ports:
- 3000:3000
depends_on:
- mysql
- redis
command: ./bin/setup-db-and-start-puma
sidekiq:
image: demoapp
env_file:
- .dockerenv/rails
depends_on:
- puma
command: ./bin/start-sidekiq
mysql:
image: mysql:5.7.21
env_file:
- .dockerenv/mysql
redis:
image: redis:4.0.9
command: redis-server --appendonly yes
コードをチェックアウトした後、下記のコマンドを実行するだけでRailsアプリが http://localhost:3000/ に起動するようになります。
$ docker-compose -f docker-compose-preview.yml up -d
ログを見たい場合は-d
オプションを外すか、下記のようにしてください。
$ docker-compose -f docker-compose-preview.yml logs -f
後始末は下記のようにします。-v
を外すとデータボリュームが残ってしまうので注意してください。
$ docker-compose -f docker-compose-preview.yml down -v
このYAMLファイルのポイントは下記の通り。
mysql
とredis
にはpuma
とsidekiq
のコンテナから接続できれば良いので、ports
(ホスト側へのポートマッピング)エントリを書きません。- コンテナ同士は特に設定をしなくても互いのポートにアクセスできます。
puma
にはbuild
エントリを追加して、docker-compose up
実行時にイメージをビルドするようにしています。- 2回目以降、イメージを強制的にビルドする際には
--build
オプションをつける必要があります。
- 2回目以降、イメージを強制的にビルドする際には
puma
はdepends_on
でmysql
を指定して、mysql
コンテナの起動後にrake db:setup
が実行されるようにしています。- ただしこれだけだと不十分なので
nc
でmysql
の3306番ポートを確認して起動するまでループします。後述。
- ただしこれだけだと不十分なので
puma
、sidekiq
、mysql
にはenv_file
を指定し、共通の環境変数をファイルで一括指定します。後述。
pumaとsidekiqのcommand
では、./bin/setup-db-start-puma
, ./bin/start-sidekiq
というコマンドをそれぞれ指定しています。
これは下記のような内容です。
# ./bin/setup-db-and-start-puma
cd $(dirname $0)/..
trap "pkill -P $$" EXIT
./bin/wait-for $MYSQL_HOST 3306
./bin/wait-for $REDIS_HOST 6379
./bin/rails db:setup_if_not_yet
./bin/pumactl start
# ./bin/start-sidekiq
cd $(dirname $0)/..
trap "pkill -P $$" EXIT
./bin/wait-for $MYSQL_HOST 3306
./bin/wait-for $REDIS_HOST 6379
./bin/sidekiq -t ${SIDEKIQ_TIMEOUT:-8}
pumaやsidekiqの起動をシェルスクリプトでラップする場合、ラッパ側のシェルがTERMやINTなどのシグナルで停止すると
子プロセスのpumaやsidekiqが起動したまま残ってしまいます。
上記のtrap
はそれを防ぐための記述で、スクリプトの停止時に子プロセスへTERMシグナルを送るように指定しています。
sidekiqの-t
オプションは実行中のジョブを強制的に停止するまでのタイムアウト値で、オプションを指定しない場合のデフォルト値は8秒です。
このスクリプトでは環境変数SIDEKIQ_TIMEOUT
でこの値を変更できるようにしています。
環境変数が存在しない場合はデフォルト値と同じ8秒が指定されます。
./bin/wait-for
は下記のような内容で、
パラメータで指定したホストのポートをnc
コマンドでチェックして接続可能になるまで待機するだけのスクリプトです。
# ./bin/wait-for
HOST=$1
PORT=$2
while :
do
nc -w 1 -z $HOST $PORT
if [[ $? = 0 ]]; then
break;
fi
sleep 1
done
db:setup_if_not_yet
は下記のようなRakeタスクで、まだrake db:setup
を実行したことがなさそうな時だけ実行します。
# lib/tasks/db.rake
namespace :db do
task setup_if_not_yet: [:environment] do
begin
ActiveRecord::Base.connection
rescue ActiveRecord::NoDatabaseError
# database not exists
Rake::Task["db:setup"].invoke
exit 0
else
if !ActiveRecord::SchemaMigration.table_exists?
# database exists but tables not exists
Rake::Task["db:setup"].invoke
exit 0
end
end
end
end
.dockerenv/rails
はpuma
とsidekiq
に共通の環境変数の設定ファイルです。
RAILS_SERVE_STATIC_FILES=true
RAILS_LOG_TO_STDOUT=true
SIDEKIQ_TIMEOUT=60
SECRET_KEY_BASE=123
MYSQL_HOST=mysql
MYSQL_USER=demoapp
MYSQL_PASSWORD=secret
MYSQL_DATABASE=demoapp_production
REDIS_HOST=redis
REDIS_URL=redis://redis:6379/1
.dockerenv/mysql
はmysql
用の環境変数の設定ファイルです。デーベース名、ユーザ名、パスワードを書いています。
MYSQL_USER=demoapp
MYSQL_PASSWORD=secret
MYSQL_DATABASE=demoapp_production
MYSQL_ROOT_PASSWORD=topsecret
公式のmysqlイメージでは、rootユーザのパスワードの他、
コンテナの起動時に作成するデータベースとそのデータベースに
アクセスできるユーザとパスワードも環境変数で指定できます。
.dockerenv/mysql
で設定している4つの環境変数はそのためのものです。
また、Railsアプリ側ではMySQLやRedisの接続先など実行環境に依存するようなパラメータを全て環境変数で 受け取れるようにしておく必要があります。
まずデータベースの設定ファイルでは、MYSQL_HOST
, MYSQL_DATABASE
, MYSQL_USER
, MYSQL_PASSWORD
で
それぞれDBのホスト名、データベース名、ユーザ名、パスワードを受け取れるようにしておきます。
(話を簡単にするため、mysql
コンテナと環境変数の名前を合わせています)
# config/database.yml
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch("MYSQL_USER") { "root" } %>
password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %>
host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %>
development:
<<: *default
database: demoapp_development
test:
<<: *default
database: demoapp_test
production:
<<: *default
database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>
また、ActionCableの設定ファイルでは環境変数 REDIS_URL
でRedisの接続先を指定できるようにしておきます。
# config/cable.yml
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: demoapp_production
Sidekiqについてはもともと環境変数 REDIS_URL
で接続先を切り替えられる仕様なので特に何もしなくて良いです。
別の方法で接続先を指定したい場合は config/initializers/sidekiq.rb
というファイルを作って設定を追加します。
詳細は下記のドキュメントを参照してください。
Using Redis mperham/sidekiq Wiki
REDIS_HOST
は前述のbin/wait-for
に渡すパラメータとして使っています。
SECRET_KEY_BASE
はCookieに改ざん検知のためのHMACダイジェストをつけるときの秘密鍵を指定するための環境変数ですが、
Rails 5.2.0のデフォルトの動作ではCredentialsで暗号化されたファイル config/credentials.yml.enc
に
書かれた値を鍵として使うようになっています。下記のコマンドでこのファイルの中身を確認できます。
% ./bin/rails credentials:show
# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: f758dcb894cf0fee1b68f7989ebd65f5fd0dbb3c366e61789c112836789dfda22def856b6419ae96447352bbdd8873b90ae0096cd61d9532775caca8c04e0783
ただ、この値は本番環境でも使う値になるので、今回のような不特定多数に配布するのが前提のプレビュー環境には使えません。
HMAC用の鍵はCredentials の値よりも環境変数SECRET_KEY_BASE
の値が優先される実装になっているので、今回は環境変数を設定します。
また、Credentialsの秘密鍵(環境変数RAILS_MASTER_KEY
またはconfig/master.key
)は設定しません。(デフォルトでは設定しなくてもエラーにはなりません。)
RAILS_SERVE_STATIC_FILES
は、jsやcssなどのファイルをpumaが応答するかどうかを指定するための設定値です。
これはRails標準の仕組みで、実装は config/environments/production.rb
を参照してください。
本番環境ではこのような静的なアセットファイルはnginxやCDNで配信するのが望ましいのですが、
今回は構成を簡単にするためにtrueにしておきます。
RAILS_LOG_TO_STDOUT
は、production
環境でRailsのログ出力先を標準入力に切り替えるための環境変数です。
これもRails標準の仕組みで、実装は config/environments/production.rb
を参照してください。
こういった環境変数設定用のファイルに本番環境の値を書く場合には、
GitリポジトリやDockerイメージに含めないように注意してください。
.gitignore
と .dockerignore
にそれらのファイル名を書いておくと意図せず混入させることを防ぐことができます。
本稿では、docker-compose-preview.yml
で構築するのはあくまでプレビュー用の環境であり、
本番環境はKubernetesで構築するという前提です。
Kubernetes上のアプリに環境変数を設定する際には全く別の方法を用いるため、
Composeファイルなどに書いた設定値は全て本番環境とは異なる値という想定なので、リポジトリにコミットしています。
また、env_file
で指定するファイルは、direnv
で使う.envrc
などシェルに環境変数を設定するスクリプトとは
根本的に異なるものなので下記の点に注意してください。
export
はつけない。- コマンドは実行できない。
- 値をクオートしてはいけない。(環境変数に
'
や"
を含む形で設定されてしまいます)
コンテナ間の通信について
コンテナにはそれぞれ固有のIPアドレスが割り当てられます。
docker container run
コマンドで起動したコンテナ同士が互いのIPアドレスを知るためにはオプション指定が必要ですが、
docker-compose
で起動したコンテナ同士は、互いのサービス名をホスト名として通信できるように自動的に設定されます。
また、ホスト側からコンテナに接続する際にはports
エントリでポートマッピングの設定を書く必要がありましたが、
docker-compose
で起動したコンテナ同士はこういった設定なしで互いの全てのポートにアクセスできます。
MySQLの接続設定をもう一度掲載します。下記のようにMYSQL_HOST
環境変数でホスト名を指定するようになっています。
# config/database.yml
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch("MYSQL_USER") { "root" } %>
password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %>
host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %>
# 省略
production:
<<: *default
database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>
.dockerenv/rails
では下記のようにmysql
という文字列をMYSQL_HOST
環境変数に設定していました。
MYSQL_HOST=mysql
このmysql
というのは、docker-compose-preview.yml
のサービス名を指しています。
version: "3"
services:
# 省略
mysql: # <- これがサービス名
image: mysql:5.7.21
# 省略
docker container run
コマンドに--link
オプションをつけると
docker-compose
が自動的に準備してくれていたようなことを手動で行うこともできますが、
Railsアプリ開発という主題においては必要になる場面はほぼ無いと思いますので、詳細は割愛します。
ローカル開発環境のRailsアプリをDocker化するかどうか
少なくともホストOSがmacOSなのであれば、 ローカルの開発環境においてはRailsアプリまでDockerコンテナの上で動かす必要はないと筆者は考えています。
- RailsアプリがmacOSで動いてLinux(コンテナ)だと動かないケースは多くない。
- 多くの場合、macOS上でRailsアプリの開発環境を用意するのはそれほど面倒ではない。
./bin/rails g
や./bin/rails c
などちょっとしたコマンドが全てdocker
コマンド経由になるのは煩雑すぎる。- springの対応が面倒
以上の理由で、私の主観ではメリットをデメリットが上回っていると感じるので、開発環境においては下記のような構成をとっています。
- Railsプロセス自体はホスト側(macOS/Linux)で直接起動する。
- Railsプロセスが接続するMySQLやRedisなどのミドルウェアは
docker-compsoe
で起動する。
参考情報
Docker Composeのより詳細な使い方は下記のドキュメントを参照してください。
docker-compose
コマンドについて- Composeファイル(
docker-compose.yml
)について
Dockerfileのより詳細な仕様については下記のドキュメントを参照してください。
おすすめの書籍は「プログラマのためのDocker教科書」です。2018/4/11に第2版が出ました。
前回のDocker編で扱った範囲も含めて、 本ドキュメントでは紹介しなかったコマンドやDockerfile/Composeファイルの機能が一通り紹介されています。 また、コンテナ技術の概要や使いどころ、プライベートレジストリやイメージの公開方法などDocker周辺を広く浅く解説してあり、 入門には良い書籍だと思います。後半ではKubernetesにも軽く触れてあります。
次回は Kubernetes入門編 です。