RailsアプリをDockerにデプロイするときにGemfileを変更してなければBundle Installをスキップする方法

http://ilikestuffblog.com/2014/01/06/how-to-skip-bundle-install-when-deploying-a-rails-app-to-docker/

1 comment | 0 points | by WazanovaNews 約3年前


Jshiike 約3年前 | ▲upvoteする | link

Brian Moreartyがタイトルの内容についてまとめています。

Dockerでは、Railsアプリを、依存関係(正しいRubyのバージョン、利用するgemなど)を組み込んだまま、コンテナにデプロイできる。アプリをそのコンテナでテストし、本番環境のホストにアップできる。事前にメモリを確保する必要がないので、かなり軽い感じのVMのようである。このポストではコンテナ作成の詳細には触れないが、簡単に説明すると、コンテナをセットアップするスクリプトであるDockerfileをつくって、docker buildで実行するというステップ。

Dockerは、最初のDockerfileのビルドを作成した後、スピードに大いに貢献するすぐれたキャッシュの仕組みを持っている。各ステップ(ファイルの各行)がそれぞれキャッシュされる。もし、10行のDockerfileの6行目を修正してビルドする場合は、1行目から5行目はスキップする。Dockerはキャッシュの結果を引っ張ってくるだけ。Rubyのコンパイルなど、とても時間のかかるプロセスをスキップできる。

しかし、RailsアプリのためにDockerを使ってみると、すぐに問題に気がつく。bundle installのステップはキャッシュできないのだ。イメージを再ビルドするたびに、Gemfileを変更してなくても、Bundlerが完了するまで待たなくてはいけない。Dockerのキャッシュによるスピードアップに慣れてしまうと、ここでイラッとしてしまう。Herokuを使ってる人だとよくわかると思う。Herokuにgit pushするたびに、Gemfileを変更してなくてもBundlerが再実行される。アセットのコンパイル以外で、Herokuにおけるデプロイの最も時間をくうところである。(HerokuはDockerを使ってないが、大元のテクノロジーはLinuxコンテナということで同じで、Dockerを使ってみて、Herokuと似たような動きをすることに気づいた。そして、Herokuがどうしてあのようなアーキテクチャを選択したのかも。)

さて、なぜDockerは他はキャッシュするのにbundle installはキャッシュしないのか。それは、バージョン0.7.3より前は、ADD instructionとその後のinstructionをキャッシュしないからである。(ADDはビルドの際に、ビルドマシンからイメージにファイルやディレクトリをコピーする。)Railsアプリをイメージに追加するには通常、最新のコードをgit pullし、ADDとともにコピーする。

DockerがADDをキャッシュしないのは、理にかなっている。コンテナにコピーするものは最新のバージョンである可能性が高いからである。しかし、それが問題のもとでもある。BundlerはGemfileに依存している。Gemfileはaddedディレクトリ(Railsアプリ)の一部であり、ディレクトリツリーは他の頻繁に修正されるファイル(ソースファイルなど)を含んでいる。よって、Bundlerは、アプリをaddした後に実行されなければいけない。つまり、bundle installのステップはキャッシュできないのである。

Well, There's Good News

「バージョン0.7.3より前」というところがポイントとなる。Docker 0.7.3は数日前にリリースされ、Railsデベロッパー向けのキラー機能がある。(同じ機能は、Pythonデベロッパーにとってのrequirements.txtにもメリットがあり、Nick Stinematesが提案するこのアプローチの代替にもなる。)**ADDコマンドがキャッシュされるようになった**のである。

ディレクトリツリーにADDすれば、Dockerは(tarアルゴリズムを使っていて、ものすごく瞬時に)、全てのファイルのコンテンツからハッシュを生成する。ファイルに変更がなければ、前回実行したdocker buildと同じADD instructionのキャッシュバージョンを利用する。これはbundle installをキャッシュできるので大きな意味がある。しかし、「ディレクトリツリーでソースを変更した後にアプリをデプロイするときはキャッシュを使えないのでは?」と思うだろうが、回避策はある。

An Example

RailsアプリにとってDockerfileがどのように見えるか確認してみる。(通常、SQLiteを本番では使わないし、データベースを同じコンテナにアプリとしては入れないと思うが、この確認作業のためには重要。)

FROM ubuntu:12.10
MAINTAINER brian@morearty.org
# Install dependencies.
RUN apt-get update
RUN apt-get install -y curl git build-essential ruby1.9.3 libsqlite3-dev
RUN gem install rubygems-update --no-ri --no-rdoc
RUN update_rubygems
RUN gem install bundler sinatra --no-ri --no-rdoc
# Copy the app into the image.
ADD railsapp /opt/railsapp
# Now that the app is here, we can bundle.
WORKDIR /opt/railsapp
RUN bundle install
# Set up a default runtime command
CMD rails server thin

docker buildをはじめて実行してみる。(Ubuntuイメージは自分のマシン上にあるので、引っ張ってくるのに時間はかからない。)Rails4アプリのデフォルトのgemを使う。

$ time docker build .
Step 1 : FROM ubuntu:12.10
 ---> b750fe79269d
Step 2 : MAINTAINER brian@morearty.org
 ---> Running in 3479b6010856
 ---> 838c7b6022ab
Step 3 : RUN apt-get update
 ---> Running in b60b17f4385c
Ign http://archive.ubuntu.com quantal InRelease
Hit http://archive.ubuntu.com quantal Release.gpg
... etc., etc. ...
Step 10 : RUN bundle install
 ---> Running in 7a57242449d7
Fetching gem metadata from https://rubygems.org/.........
Fetching additional metadata from https://rubygems.org/..
Installing rake (10.1.1)
Installing i18n (0.6.9)
Installing minitest (4.7.5)
...
real    2m18.260s

最初のビルドには2分18秒。次にソースファイルを修正(Gemfileは修正しない。)し、再度docker buildしてみる。ADDのキャッシュをサポートしているバージョン0.7.3を使う。ソースファイルが一つ修正されたので、アプリディレクトリ全体が修正されたと見なされる。よって、Dockerはキャッシュされたバージョンを使わない。bundle installはADDの後に起きるので、キャッシュされなかったステップの後にくるステップもキャッシュされない。

$ time docker build .
Uploading context 337.9 kB
Uploading context
Step 1 : FROM ubuntu:12.10
 ---> b750fe79269d
Step 2 : MAINTAINER brian@morearty.org
 ---> Using cache
 ---> 5895ed9e78a4
etc., etc. ...
Step 10 : RUN bundle install
 ---> Running in 3f0ddbeea83e
Fetching gem metadata from https://rubygems.org/.........
Fetching additional metadata from https://rubygems.org/..
Installing rake (10.1.1)
Installing i18n (0.6.9)
Installing minitest (4.7.5)
...
real    0m55.596s

55秒。Gemfileは修正しなかったのに、ほとんどの時間はbundle installでかかっている。

I Like Stuff that's Cached

古いバージョンのDockerであれば諦めるしかない。しかし、新しいバージョンでは、Dockerファイルに数行追加することでbundle installをキャッシュすることができる。13〜16行目を注目。

1  FROM ubuntu:12.10
2  MAINTAINER brian@morearty.org
3 
4  # Install dependencies.
5  RUN apt-get update
6  RUN apt-get install -y curl git build-essential ruby1.9.3 libsqlite3-dev
7  RUN gem install rubygems-update --no-ri --no-rdoc
8  RUN update_rubygems
9  RUN gem install bundler sinatra --no-ri --no-rdoc
10 
11 # Copy the Gemfile and Gemfile.lock into the image. 
12 # Temporarily set the working directory to where they are. 
13 WORKDIR /tmp 
14 ADD railsapp/Gemfile Gemfile
15 ADD railsapp/Gemfile.lock Gemfile.lock
16 RUN bundle install 
17 
18 # Everything up to here was cached. This includes
19 # the bundle install, unless the Gemfiles changed.
20 # Now copy the app into the image.
21 ADD railsapp /opt/railsapp
22 
23 # Set the final working dir to the Rails app's location.
24 WORKDIR /opt/railsapp
25 
26 # Set up a default runtime command
27 CMD rails server thin

アプリ全体をコピーする前に、GemfileとGemfile.lockをtmpディレクトリにコピーし、そこからbundle installを実行する。どちらのファイルも変更されてなければ、両方のADD instructionはキャッシュされる。これで、bundle installなどのコマンドはキャッシュを利用できるようになる。bundleした後でアプリの残りをイメージにコピーする。この後のステップはキャッシュされないので、なるべく後の段階でやるようにしたい。(CMDステップを更に動かすこともできたが、そもそも早いのであまり結果に影響はない。)ビルドをして時間を計ってみよう。docker build全体のアウトプットを見せるので、全てがキャッスされていることがわかると思う。33行目を確認されたし。bundle installコマンドがキャッシュされたことを示している。

1  Uploading context 337.9 kB
2  Uploading context
3  Step 1 : FROM ubuntu:12.10
4   ---> b750fe79269d
5  Step 2 : MAINTAINER brian@morearty.org
6   ---> Using cache
7   ---> 5895ed9e78a4
8  Step 3 : RUN apt-get update
9   ---> Using cache
10  ---> d2898351463e
11 Step 4 : RUN apt-get install -y curl git build-essential ruby1.9.3 libsqlite3-dev
12 ---> Using cache
13  ---> aa1dbf3e6452
14 Step 5 : RUN gem install rubygems-update --no-ri --no-rdoc
15  ---> Using cache
16  ---> 8f4ef4bcfd32
17 Step 6 : RUN update_rubygems
18  ---> Using cache
19  ---> 358ef92178c7
20 Step 7 : RUN gem install bundler sinatra --no-ri --no-rdoc
21  ---> Using cache
22  ---> 9e7d9c0fd7de
23 Step 8 : WORKDIR /tmp
24  ---> Using cache
25  ---> b10a5c9f12c0
26 Step 9 : ADD railsapp/Gemfile Gemfile
27  ---> Using cache
28  ---> 79deb268175e
29 Step 10 : ADD railsapp/Gemfile.lock Gemfile.lock
30  ---> Using cache
31  ---> 1315e65cb616
32 Step 11 : RUN bundle install
33  ---> Using cache
34  ---> 6f067cbf6c2f
35 Step 12 : ADD railsapp /opt/railsapp
36  ---> 655d668c338d
37 Step 13 : WORKDIR /opt/railsapp
38  ---> Running in 0272330053b5
39  ---> 94dda8e65416
40 Step 14 : CMD rails server thin
41  ---> Running in 9afb1cee2bcf
42  ---> 1429538cbdfb
43 Successfully built 1429538cbdfb
44 
45 real    0m17.974s
46 user    0m0.000s
47 sys     0m0.020s

前回55秒に対して、今回18秒。最後に、Gemfileを変更したら、Dockerはキャッシュを使わないことを確認しておこう。

$ touch railsapp/Gemfile
$ time docker build .
Uploading context 337.9 kB
Uploading context
Step 1 : FROM ubuntu:12.10
 ---> b750fe79269d
Step 2 : MAINTAINER brian@morearty.org
 ---> Using cache
etc. etc. ...
Step 10 : ADD railsapp/Gemfile.lock Gemfile.lock
 ---> f5a40ceac4ce
Step 11 : RUN bundle install
 ---> Running in 3095386f3f46
Fetching gem metadata from https://rubygems.org/.........
Fetching additional metadata from https://rubygems.org/..
Installing rake (10.1.1)
Installing i18n (0.6.9)
Installing minitest (4.7.5)
etc. etc. ...
real    1m5.819s

Gemfileに手をいれたので、bundle installが再度実行され、1分5秒という結果になった。


[2013] ワザノバTop100アクセスランキング


#rails #docker #コーディング #devops #自動化

Back