Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

記事のボリュームが大きいので前編/後編に分割しました。後編は来週公開予定です。

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerと無関係なツールを必要としません。

はじめに

本チュートリアルでは、Herokuにデプロイする感覚でソフトウェアをシンプルに自動デプロイするツールの作成方法をご紹介します。デプロイごとのバージョン管理にはDockerを使い、アップグレードやロールバックをやりやすくします。また、アプリの継続的デプロイ(CD)に弊社のSemaphoreも使います。

コンテナは任意のDocker Registryにホスティングできます。アプリの実行に必要なのはDockerがインストールされているホストだけです。

チュートリアルを終えると、サンプルアプリをリモートホストにHerokuと同じようにデプロイできるシンプルなRuby CLIスクリプトが使えるようになります。直前のバージョンへのロールバック、ログの追加、実行中のアプリのバージョントラッキングを行えるコマンドもあります。

本チュートリアルはデプロイ手順を中心に据えていますので、用途に応じて環境を調整すれば任意のアプリで使えます。ここではシンプルなHello WorldをRuby on Railsでblogフォルダに構築します。Railsアプリ構築の初歩については、RailsガイドのRails をはじめようの手順1〜4をご覧ください。

必要なもの

  • Docker: ホストと、アプリをデプロイするすべてのマシンにDockerがインストールされ、動作している必要があります。
  • Docker Registryのアカウント(Docker Hubなど)
  • SSHアクセスが可能でDockerがインストールされているクラウドプロバイダ(AWS EC2など)
  • Ruby 2.3: アプリをデプロイするすべてのマシンにインストールされている必要があります。

デプロイの手順

デプロイの手順は次の5つで構成されます。

  • ビルド: いつでも変更可能なビルド手順を備えた独自のコンテナをアプリごとにビルドします。
  • アップロード: アプリのコンテナのビルドが終わったらDocker Registryに送信する必要があります。初回はコンテナ全体のアップロードが必要なので多少時間がかかりますが、次回からはDockerのレイヤシステムでサイズや帯域を節約できるので速くなります
  • 接続: Docker Registryにコンテナを送信したら、次の手順を行うためにホストに接続します。
  • ダウンロード: ホストに接続したら、コンテナをダウンロードします。
  • 再起動: 最後の手順では、アプリを停止し、続いて停止時と同じ設定(ポート、ログ、環境変数など)で新しいコンテナを起動します。

手順の概要を把握できたので、作業を開始しましょう。

1. コンテナのビルド

この例では、アプリを実行するコンテナを1つ使います(Rubyはコンパイル言語ではないのでいわゆるビルドは不要です)。この場合のDockerファイルは次のとおりです。

FROM ruby:2.3.1-slim

COPY Gemfile* /tmp/
WORKDIR /tmp

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

CMD cd /tmp &&\
    tar -xzf application.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

スクリプトを整理するために、Dockerfileはアプリの1つ上のフォルダ階層に置きます。次のような構成になります。

.
├── Dockerfile
├── blog
│   ├── app
│   ├── bin
... (アプリのファイルやフォルダ)

Dockerfileの各行について解説します。

FROM ruby:2.3.1-slim

これはコンテナのビルドに使うベースイメージです。Rubyがインストールされている必要があるので、自分で全部インストールするよりもプレインストール済みのコンテナを使う方が楽です。

COPY Gemfile* /tmp/
WORKDIR /tmp

ここでは、GemfileとGemfile.lockをコンテナの/tmpディレクトリにコピーし、次のコマンドを実行する/tmpに移動しています。

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

このRubyイメージのbundlerは古いので、warning表示を避けるためにアップデートしています。本チュートリアルで使われているのとは別のアプリで作業する場合は、他にもいくつかのパッケージ(多くはコンパイラ)が必要になるでしょう。最後にGemfileのgemをすべてインストールします。

Dockerの各コマンドは(layerなど)、コマンドの結果が同じ場合に再実行を避けるためにキャッシュされます。これで多少時間を節約できます。--pathフラグは、すべてのgemをローカルの定義済みパス(vendor/bundle)にインストールするよう指示します。

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

ここでは、bundlerの最終的なインストールパスを作成し、インストールされたgemを前回のビルドキャッシュからすべてコピーしてから、圧縮されたアプリをコンテナ内にコピーします。

CMD cd /tmp &&\
    tar -xzf build.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

このコマンドはdocker runコマンドのときに実行されます。コンテナ内部の圧縮されたアプリを展開し、セットアップ手順(migrate)を実行してアプリを起動します。

Dockerfileの設定どおりに動作していることを確認するには、Dockerfileのあるrootディレクトリに移動して次のコマンドを実行します。

: 以下のmydockeruserはDocker Registryの登録済みユーザー名です。これは後でコンテナのバージョン管理に用います。

注2: Railsをproduction環境で実行する場合は、config/secrets.ymlファイルでSECRET_KEY_BASEなどの環境変数が必要です。ここでは単なるサンプルアプリを使っているので、development環境やtest環境と同様に固定値で安全に上書きできます。

$ cp blog/Gemfile* .
$ tar -zcf application.tar.gz blog
$ docker build -t mydockeruser/application-container .

上を実行すると、Dockerfileの各手順のビルドが以下のように開始されます。

Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> f103f7b71338
Removing intermediate container 78bc80c13a5d
Step 3/9 : WORKDIR /tmp
 ---> f268a864efbc
Removing intermediate container d0845585c84d
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Running in dd634ea01c4c
Successfully installed bundler-1.14.6
1 gem installed
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [453 kB]
...

すべて問題なく完了すると、以下の成功メッセージが表示されます。

Successfully built 6c11944c0ee4

このハッシュ値はDockerによってランダムに生成されるので、コンテナをビルドするたびに異なります。

キャッシュが効いていることを確認するために、同じコマンドを再実行してみましょう。今度はほぼ一瞬で完了します。

$ docker build -t mydockeruser/application-container .
Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> Using cache
 ---> 08d525aa0168
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Using cache
 ---> ce28bd7f53b6
Successfully built ce28bd7f53b6

エラーメッセージが表示されたら、Dockerfileの構文やコンソールエラーをチェックしてやり直します。

次はすべて問題ないことを確認するために、コンテナのアプリを実行できるかどうかをテストしたいと思います。以下のコマンドを実行します。

docker run -p 3000:3000 -ti mydockeruser/application-container

これはコンテナを実行し、ホストのポート番号3000をコンテナのポート番号3000にマッピングします。問題が起きなければ次のようなRails起動メッセージが表示されます。

=> Booting Puma
=> Rails 5.0.2 application starting in production on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.1-p112), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

これで、localhost:3000をブラウザで開けばWelcomeメッセージが表示されます。

2. コンテナをDocker Registryにアップロードする

Docker Registryにログインするには以下の手順が必要です。

> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.
Username: mydockeruser
Password: ########
Login Succeeded

コンテナは完全に動作するので、今度はこれをDocker Registryにアップロードする必要があります。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Pushing [==================================================>] 352.8 kB
08ee50f4f8a7: Preparing
33e5788c35de: Pushing  2.56 kB
c3d75a5c9ca1: Pushing [>                                                  ] 1.632 MB/285.2 MB
0f94183c9ed2: Pushing [==================================================>] 9.216 kB
b58339e538fb: Waiting
317a9fa46c5b: Waiting
a9bb4f79499d: Waiting
9c81988c760c: Preparing
c5ad82f84119: Waiting
fe4c16cbf7a4: Waiting

コンテナはレイヤごとにアップロードされますが、中には巨大なものもあります(100MB以上)初回に巨大なレイヤがアップロードされるのは問題ありません。今後はDockerのレイヤシステムを用いてアプリの変更分だけをアップロードし、ディスク容量や帯域を節約します。docker pushやレイヤについて詳しくお知りになりたい方は、公式ドキュメントをご覧ください。

pushが終わると成功のメッセージが表示されます。

...
9f5e7eecca3a: Pushed
08ee50f4f8a7: Pushed
33e5788c35de: Pushed
c3d75a5c9ca1: Pushed
0f94183c9ed2: Pushed
b58339e538fb: Pushed
317a9fa46c5b: Pushed
a9bb4f79499d: Pushed
9c81988c760c: Pushed
c5ad82f84119: Pushed
fe4c16cbf7a4: Pushed
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

Docker Registryコンソールで新しいイメージを確認できます。

イメージを再度pushしてみると、すべてのレイヤが既に存在することがわかります。Dockerは再アップロードを回避するために、各レイヤのハッシュを照合してレイヤが既にあるかどうかをチェックします。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Layer already exists
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

3. リモート接続を開く

コンテナのアップロードが終わったので、リモートサーバーにダウンロードして実行する方法を見てみましょう。最初に、コンテナを実行するリモート環境の準備が必要です。ホストマシンで行ったときと同様に、DockerをインストールしてDocker Registryにログインしなければなりません。SSHでリモート接続するには、以下のコマンドを実行します。

ssh remoteuser@35.190.185.215
# 認証が必要な場合は以下を実行
ssh -i path/to/your/key.pem remoteuser@35.190.185.215

4. ダウンロード

リモートマシンでの設定をすべて終えた後は、ターミナルでのアクセスは不要になります。各コマンドはその環境で実行されます。コンテナをダウンロードしましょう。必要な場合はキーのフラグを指定することもお忘れなく。

$ ssh remoteuser@35.190.185.215 docker pull mydockeruser/application-container
Using default tag: latest
latest: Pulling from mydockeruser/application-container
386a066cd84a: Pulling fs layer
ec2a19adcb60: Pulling fs layer
b37dcb8e3fe1: Pulling fs layer
e635357d42cf: Pulling fs layer
382aff325dec: Pulling fs layer
f1fe764fd274: Pulling fs layer
a03a7c7d0abc: Pulling fs layer
fbbadaebd745: Pulling fs layer
63ef7f8f1d60: Pulling fs layer
3b9d4dda739b: Pulling fs layer
17e2d6aad6ec: Pulling fs layer
...
3b9d4dda739b: Pull complete
17e2d6aad6ec: Pull complete
Digest: sha256:c030e4f2b05191a4827bb7a811600e351aa7318abd3d7b1f169f2e4339a44b20
Status: Downloaded newer image for mydockeruser/application-container:latest

5. 再起動

コンテナを初めて実行したので、他のコンテナを停止する必要はありません。ローカルホストのときと同じコマンドを使って次のようにコンテナを実行できます。

$ ssh remoteuser@35.190.185.215 docker run -p 3000:3000 -d mydockeruser/application-container
f86afaa7c9cc4730e9ff55b1472c5b30b0e02055914f1673fbd4a8ceb3419e23

ここでは-tiフラグの代わりに-dフラグを与えているので、コンテナのハッシュだけが出力されます。これはコンテナをdetachedモードで動かす(出力をターミナルにアタッチしない)ことを表します。

ブラウザでリモートホストアドレス(ここでは35.190.185.21:3000を開いて、アプリが実行されているかどうかをチェックします。

(後編に続きます)

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

Dockerでsupervisorを使う時によくハマる点まとめ

PrometheusでDockerホスト + コンテナを監視してみた

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ