Rails5の開発環境をDockernizeする(MySQL + Spring + Sidekiq + Redis + docker-sync)

年の瀬など関係なく過ごしております。

既存のRailsアプリの開発環境をDocker化する機会がありましたので、やったことを記録しておきます。

とは言っても、先人たちの知恵がかなり参考になりましたので、それを組み合わせて調整した程度の焼き直しです。

前提

  • docker-compose up 一発で開発環境構築が済むようにしたい
  • 本番環境のコンテナ化は別のDockerfile & docker-compose.ymlで行う

参考にしたページ(一部)

とても参考になりました。ありがとうございます。

成果物

Dockerfile.dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
FROM ruby:2.4.2

ENV LANG C.UTF-8

RUN apt-get update -qq
RUN apt-get install -y --no-install-recommends \
build-essential \
curl \
wget \
apt-transport-https \
git \
libpq-dev \
libfontconfig1 && \
rm -rf /var/lib/apt/lists/*

# install node
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
RUN apt-get install -y nodejs

# install yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update
RUN apt-get install -y yarn

ENV ENTRYKIT_VERSION 0.4.0

RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
&& mv entrykit /bin/entrykit \
&& chmod +x /bin/entrykit \
&& entrykit --symlink

RUN mkdir /app

WORKDIR /app

RUN bundle config build.nokogiri --use-system-libraries

ENTRYPOINT [ \
"prehook", "ruby -v", "--", \
"prehook", "bundle install -j3 --quiet", "--", \
"prehook", "yarn install", "--"]

ポイント:

  • nodeのインストール
    • railsアプリでuglifierなどのExecJSが必要なgemをいくつか使っていますので、予めnodeをインストールしておきます
  • yarnのインストール
    • こちらも同様に、railsアプリ内で利用するnpmモジュールを package.json で管理しておりますので、予めyarnをインストールしておきます
  • ENTRYKITの活用
    • 参考サイトそのままですが、ENTRYKITを活用することで、起動時に必要な処理(ここでは bundle installyarn install)などを完結に記述できます

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
version: '2'
services:
rails: &app_base
build:
context: .
dockerfile: "Dockerfile.dev"
command: ["bundle", "exec", "rails", "s"]
env_file:
- "./.env.dev"
volumes:
- "rails-sync:/app"
volumes_from:
- data
ports:
- "3000:3000"
- "9292:9292"
depends_on:
- db
tty: true
stdin_open: true
worker:
<<: *app_base
command: ["bundle", "exec", "sidekiq"]
ports: []
depends_on:
- db
- redis
db:
image: "mysql:5.7.20"
environment:
MYSQL_ROOT_PASSWORD: password
volumes_from:
- data
ports:
- "3306:3306"
redis:
image: redis:alpine
networks:
- default
ports:
- '6379:6379'
volumes_from:
- data
data:
image: busybox
volumes:
- "db:/var/lib/mysql"
- "bundle:/usr/local/bundle"
- "redis:/data/redis"
- "node_modules:/app/node_modules"

volumes:
db:
driver: local
bundle:
driver: local
redis:
driver: local
node_modules:
driver: local
rails-sync:
external: true

ポイント:

  • railsコンテナのport 9292

    • config/puma.rb 内でSSL with オレオレ証明書を有効にしているので、ポートを空けています

      1
      2
      3
      4
      5
      6
      if "development" == ENV.fetch("RAILS_ENV") { "development" }
      ssl_bind '0.0.0.0', '9292', {
      key: 'dev-cert/server.key',
      cert: 'dev-cert/server.crt',
      verify_mode: 'none'
      }
  • 各種データストア, bundle, node_modulesの永続化

    • busybox imageを使って、永続化が必要なデータを隔離しています
    • 新しいモジュールを追加しても、bundle installやyarn installの実行は短い時間で済みます
  • springコンテナは不要だった
    • 上記参考サイトにはspringコンテナが別途必要とのことだったのですがbin/railsbin/rake 内にspringを読み込むコードがあれば、docker-compose exec rails rails cでSpring preloader経由で呼び出されました
    • なぜだろう
  • worker / sidekiqコンテナ
    • sidekiqを起動するために、別でコンテナを定義しています
  • 今だったら、version: 3の記法で書いてもよかったかもしれない

docker-syncについて

docker-sync.yml

1
2
3
4
5
6
7
8
9
version: "2"

options:
verbose: true
syncs:
rails-sync: # tip: add -sync and you keep consistent names as a convention
src: '.'
# sync_strategy: 'native_osx' # not needed, this is the default now
# sync_excludes: ['ignored_folder', '.ignored_dot_folder']

Docker for Macにはファイルシステムのパフォーマンス上の問題があるため、workaroundとして、次の2種類が有力です。

  1. cachedオプションの使用 参考
    • volumeマウント時にどのレベルでファイルの一貫性を要求するか、というオプションが設定されました
    • 設定して試してみましたが私の環境では大きな効果はなかったので、docker-syncを採用しました
  2. docker-sync

    • 上記の通り、docker-sync.ymlという設定ファイルにrails-syncという名前でdocker-sync用のvolumeを作成します
    • railsコンテナからは、rails-sync volumeをマウントします
    1
    2
    volumes:
    - "rails-sync:/app"
    • 今回は、開発環境のみでしかdocker-compose.ymlを使用していませんが、もしdocker-composeが本番用にもある場合はこちらのテクニックが参考になります
    • docker-syncの設定ファイルには多数のオプションがありますので、気になる方は公式ページを確認するとよいでしょう。
    • 手元の環境では、docker-syncを使用することでページ表示までの時間が約3倍ほど高速化(2s -> 0.7s)されました。よかったですね。

まとめ

こういう作業をすべてゼロからやっていたと思うと恐ろしいですが、先人の知恵のおかげで1日で快適な開発環境を手に入れることができました。

みなさまよいお年を。


KintoneでWebpack + Babel + Vueを使ってモダンに爆速開発していくサンプル(1/2)

背景

Kintoneのカスタマイズを始めたのですが、開発を進めるにあたって「1つのアプリに対して、JSファイルやCSSファイルをどんどんアップロードしていく仕組みだと開発スピードが落ちる」ということを強く感じました。その理由としては、以下の通りです。

  • 使いたいライブラリが出て来るたびにいちいちアップロードするのが面倒
  • JSファイルの構成を変えたときもアップロードし直さないといけない
  • 複数開発者が1つのアプリに対して作業するときにコンフリクトしやすい

なお、webpack + Babelの導入だけであればKintone devcamp 2017のセッションのスライドがステップバイステップで説明してあり、かなりわかりやすいです。

Webpackを導入するぞ!

上記の問題を受けて、現代のフロントエンド開発では欠かせないWebpackを導入することにしました。

Webpackとは、複数ファイル(Javascript、css、テンプレートなどなど)を1つのファイルにいい感じにまとめて(Bundleと呼びます)くれるツールのことです。(他にわかりやすい記事がたくさんあるので詳しく知りたい方は検索しましょう)

例:
顧客が一覧になっている画面に、メール送信用のボタンをそれぞれに追加する状況を考えましょう。メール送信用ボタンを押すと宛先がすでに入力された状態でポップアップが立ち上がり、メールのタイトル、本文が編集可能になる、とします。

サンプル画面

通常であれば、1つのJavascriptファイルに、

  • メール送信ボタンを配置するためのDOMを検索する処理
  • ボタンのHTMLとそれを配置する処理
  • ボタンが押されたときに表示されるポップアップのHTMLテンプレート
  • etc…

という風にゴリゴリ処理を書き、それをJavascriptファイルとしてKintoneにアップロードすることでしょう。どうでしょう、辛い未来が見えませんか?そこで登場するのがWebpackです。

更に今回はWebpackのメリットをより実感するためにVuejsKeenUIもついでに入れてみたいと思います。

まずはシンプルにボタンを置く

Kintoneのサンプルによくあるこんな感じでKintoneに読み込ませて、顧客詳細情報を表示します。
「メール送信」ボタンが1つページ上部に表示されるはずです。

popup-email-simple.js

1
2
3
4
5
6
7
8
9
10
11
(function () {
'use strict';
var events = ['app.record.detail.show'];
kintone.events.on(events, function (event) {
var se = kintone.app.record.getHeaderMenuSpaceElement();
var btn = document.createElement("BUTTON");
var t = document.createTextNode("メール送信");
btn.appendChild(t);
se.appendChild(btn);
});
})();

そろそろwebpackでも入れようか、、、

ボタンは無事表示できましたが、このまま拡張していくのは早々に諦め、Webpackを使う方向に転換します。

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "kintone-webpack-sample",
"version": "1.0.0",
"scripts": {
"webpack": "webpack",
"serve": "webpack-dev-server --https --inline --hot --port=8081"
},
"author": "Yoshiki Ozaki",
"dependencies": {
"glob": "^7.1.2",
"keen-ui": "^1.0.0",
"vue": "^2.3.4"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.24.1",
"style-loader": "^0.18.2",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vue-loader": "^12.2.1",
"vue-template-compiler": "^2.3.4",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.4.5"
}
}

大事な部分は、webpackとbabel*の部分です。また、ついでにvue,keen-ui、その他必要そうなloaderを追加しておきます。

.babelrc

1
2
3
{
"presets": ["es2015"]
}

webpack.config.babel.js

webpackの設定ファイルもes2015で書きたい!ということで、webpack.config.bable.jsというファイル名にしておきます。
entryでwebpackに渡したいファイルを指定、outputでwebpackがバンドルしたファイルの置き場を指定します。

loadersセクションで、拡張子ごとにどのように処理していくか(どのloaderに処理させるか)を指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const glob = require('glob');
export default {
// entry: glob.sync('./src/*.js'),
entry: './popup-email-simple.js',
output: {
path: `${__dirname}/dist`,
filename: 'bundle.js'
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
loader: 'style-loader!css-loader'
}
]
},
resolve: {
alias: { vue: 'vue/dist/vue.js' }
},
};

ちなみにBabelとは

開発者:「新しいJavascriptの書き方を使いたい!」「でもまだサポートしてないブラウザもあるしなー」

Babel:「わいがトランスパイルしてどのブラウザでも動くようにしといたるやでー」

という存在です。

とうとうwebpack実行

  1. $ npm run webpack
    • ずらっとファイル名が出力され、コンパイルされます。
  2. cat dist/bundle.js
    • コンパイルされて、新しくbundle.jsが出来ていることを確認します。
  3. できあがったbundle.jsをKintoneにアップロードして動くことを確認

それでもKintoneに毎回アップロードするの面倒じゃない?

webpackでバンドルしたとしても、javascirptファイルをアップロードすることには変わりありません。そこで便利なのが、webpack-dev-serverコマンドです。

上記package.jsonにもありますが、このように設定しておくと、webpackでバンドルしたファイルをhttps経由(※KintoneにURLでJavascriptを読み込ませるにはSSLが必須のため)で配信してくれます。

package.json

1
2
3
4
5
6
7
8
9
10
11
{
.......
"scripts": {
"webpack": "webpack",
"serve": "webpack-dev-server --https --inline --hot --port=8081"
},
.........
"devDependencies": {
"webpack-dev-server": "^2.4.5"
}
}

npm run serveというコマンドを叩くことで、https://localhost:8081/index.jsというアドレスでバンドルされたファイルが取得できます。

hotオプションのおかげで、webpackが処理しているファイルを更新すると、すぐに反映されますので、開発中のファイル更新→画面で確認、というサイクルが爆速でで回せるようになります。これはかなり便利ですので、ぜひお試しください。

※注意:ssl証明書などを準備していないため、chromeなどのブラウザでは接続を拒否されてしまい、Kintoneからうまく読み込めないことがあります。その際は一度localhodtのURLを直接入力して、警告を無視する設定にしておくと、Kintoneでもうまく動作します。警告を出さずにやりたい方は、--keyオプションや--certオプションを併用されるとよいでしょう。

長くなってきたので、Vueの導入は次回後編で。