【翻訳】DockerとVagrantによるRails開発環境

  • 493
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

オリジナル:"A Rails Development Environment with Docker and Vagrant" by Ben Dixon
http://www.talkingquickly.co.uk/2014/06/rails-development-environment-with-vagrant-and-docker/

(デプロイ経験あまりないので訂正、つっこみなど大歓迎です。訳しづらかったところは原文も入れてます。Benさんから翻訳&&シェアOKの許諾もらっています。このテーマでさらにいくつか記事を投稿していく予定らしいです。)

既存のRailsプロジェクトに新しい開発者を招き入れることは、本来あるべき簡単さよりもまだまだはるかに難しいタスクです。アプリやアプリ一式の開発環境構築作業の大部分(正しいバージョンのrubyやデータベースその他を選びローカル環境で動かすetc...)は、多くの場合丸1日かかります。VagrantとDockerの組み合わせでこれらの作業を過去のものにすることができます。

これらの作業を緩和するためにVagrant単体でも多くのことを成し得てきていますが、Dockerと併用することでさらにもう一歩先に進むことができます。完全に機能する開発環境を(新規プロジェクトや既存のプロジェクト両方で)ものの1分で手に入れることができるだけでなく、開発環境で作ったコンテナをほぼそのまま本番環境へデプロイすることができます。さらにこれによって、常に繰り返されてきた「開発環境では動いたのに」という問題を回避することさえできます。

このチュートリアルでは、RailsとPostgres、Redisを組み合わせた完全に機能する開発環境を構築するために、VagrantとDockerをどのように使うかを説明します。次回のチュートリアルでは、今回ここで作ったコンテナをどのように本番環境へデプロイするかをお見せします。

VagrantとDockerのプロバイダ

Vagrantはバージョン1.6でプロバイダとしてのDockerのネイティブサポートを追加しました。あなたがもしLinuxマシンで開発をしているなら、そのマシンではネイティブにDockerが動きます。そうでなければ当然のことながらバーチャルマシンをDockerのホストとして動かすことになります。

このチュートリアルでは、プロバイダが提供するDSLは使用しません。これには2つの理由があります。

  • 本番環境用のDocker設定の仕様を考える段階になった時に、Dockerのオプションやコマンドラインスイッチを理解しておくことはとても重要です。VagrantプロバイダはこれをDSLに抽象化し、そのDSLは(ruby向きなやり方にはなっているが)Docker自体が持つコマンドラインスイッチと同程度の難しさです。
  • Vagrantを使って本番環境のVMと同環境のUbuntuVMを構築することで、開発時の設定が可能な限り本番環境での設定と同一に近いものになっている状態にします。

ということで、標準的なUbuntuVMをVagrantで構築してDockerをインストールします。その他のことはDockerのシェルコマンドを使ってやります。

目標とする成果

最終的なシステムでは、完全なDockerベースの開発環境の構築を以下のシンプルなコマンドだけでセットアップできるようになります。

vagrant up

これらの開発環境の構成は以下になります。

  • DockerがインストールされたUbuntuが動いているバーチャルマシン
  • RailsアプリケーションとPostgreSQLとRedisのための分離されたDockerコンテナ
  • Dockerコンテナにリンクされた共有フォルダ。今と同じように開発マシン内のRailsのコードを編集し、変更内容を http://localhost:3000 に即反映させるため。
  • Docker環境の中で、通常のRailsコマンド(rake db:migraterails c etc..)を動かすためのシンプルなインターフェイス

手順

必要条件

Dockerとは何か?(原文:I'm assuming a basic understanding of what Docker is.)。もしあなたがDockerのことをはじめて耳にしたのであれば、Dockerのサイトにはインタラクティブですばらしい基礎チュートリアルがあります。 https://www.docker.io/gettingstarted/

私は今から説明するチュートリアルをOSXとUbuntu12.04で試しました。他のnix系でも動くはずですが、windowsの場合はもっと細かな調整が必要になるかもしれません。

アプリのDocker化

設定

標準的なRails 4.1.0のアプリケーションをrails newで作成することから始め、ひとつのModelを追加しScaffold、そしてPostgreSQLを使用するように変更しました。最終的なソースコードは以下にあります。
https://github.com/TalkingQuickly/docker_rails_dev_env_example

このRailsアプリでは、すべての非公開の値(APIキーやsecret.ymlの中のすべて、など)を環境変数の中に保持し、dotenv gemを使って開発環境に読み込んでいます。このチュートリアルのアプリケーション例では、.envファイルがバージョン管理の中に含まれていることに注意してください。実際に運用するアプリケーション、特に公開リポジトリの中のアプリケーションの場合はこれらの値は.gitignoreファイルに追加されるべきです。

PostgreSQLのアクセスの詳細はデータベースコンテナから直接推測されます(原文:The PostgreSQL access details will be inferred directly from the database container)。これに関しては後述する「リンクされたコンテナ内の環境変数」のセクションを参照してください。このアプリケーション例では、database.ymlは以下のようになっています。

database.yml
default: &default
  adapter: postgresql
  pool: 5
  timeout: 5000

development:
  <<: *default
  encoding: unicode
  database: dpa_development
  pool: 5
  username: <%= ENV['DB_ENV_POSTGRESQL_USER'] %>
  password: <%= ENV['DB_ENV_POSTGRESQL_PASS'] %>
  host: <%= ENV['DB_PORT_5432_TCP_ADDR'] %>

test:
  <<: *default
  encoding: unicode
  database: dpa_test
  pool: 5
  username: <%= ENV['DB_ENV_POSTGRESQL_USER'] %>
  password: <%= ENV['DB_ENV_POSTGRESQL_PASS'] %>
  host: <%= ENV['DB_PORT_5432_TCP_ADDR'] %>

Dockerfileとスクリプト

この設定例では3つのDockerfileが必要になります。Rails用、Redis用、PostgreSQL用です。Rails用のDockerfileはRailsプロジェクトのルート階層に保存します。他のものはdocker/ディレクトリのサブフォルダに保存します。

これらのファイルのテンプレートは以下のリンクにあります。
https://github.com/TalkingQuickly/docker_rails_dev_env

始めるにあたって、このリポジトリからすべてのファイルとフォルダをあなたのRailsプロジェクトのルート階層にコピーしてください。すでに存在するプロジェクトに追加したい場合は、Dokcerに関するファイルとフォルダを以下のように配置してください。

├── Dockerfile
├── Vagrantfile
├── d
└── docker/
    ├── postgres/
    ├── rails/
    ├── redis/
    └── scripts/

Vagrantのセットアップ

もしあなたが初めてVagrantのことを耳にしたなら、Vagrantのサイトのgetting startedのチュートリアルをざっと通してみるといいでしょう。 http://docs.vagrantup.com/v2/getting-started/ 少なくともprovisioningのセクションの終わりまでです。

Vagrantfile

VagrantfileはRailsプロジェクトのルート階層に保存され、以下のような内容になります。

# Dockerの稼働環境構築に必要なコマンド
# コンテナへのリンク、など
$setup = <<SCRIPT
# 既存コンテナの停止と削除
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)

# Dockerfilesからコンテナを作成
docker build -t postgres /app/docker/postgres
docker build -t rails /app
docker build -t redis /app/docker/redis/

# コンテナの実行とリンク
docker run -d --name postgres -e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker postgres:latest
docker run -d --name redis redis:latest
docker run -d -p 3000:3000 -v /app:/app --link redis:redis --link postgres:db --name rails rails:latest

SCRIPT

# VM再起動時に正しいDockerコンテナを開始するために必要なコマンド
$start = <<SCRIPT
docker start postgres
docker start redis
docker start rails
SCRIPT

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure("2") do |config|

  # 構築に必要なリソース
  config.vm.provider "virtualbox" do |v|
    v.memory = 2048
    v.cpus = 2
  end

  # NFS共有を動かすためにプライベートネットワークが必要
  config.vm.network "private_network", ip: "192.168.50.4"

  # RailsサーバのPort転送
  config.vm.network "forwarded_port", guest: 3000, host: 3000

  # Ubuntu
  # config.vm.box = "precise64"
  # ↓
  # こちらのフォーマットに仕様が変更になっている、とのことで修正(2014.7.27)
  config.vm.box = "hashicorp/precise64"

  # 最新のdockerをインストール
  config.vm.provision "docker"

  # ここはNFSを使う。でなければrailsのパフォーマンスは大きく落ちる
  config.vm.synced_folder ".", "/app", type: "nfs"

  # VM初回作成時にコンテナを設定する
  config.vm.provision "shell", inline: $setup

  # VM起動時に常に正しいコンテナが実行されるように
  config.vm.provision "shell", run: "always", inline: $start
end

Vagrantの共有フォルダ

Dockerコンテナがローカルのファイルシステムから直接Railsアプリを使うようにする必要があります。私たちがいつもやっているように、ソースを編集しその後変更内容が開発環境のサーバーにすぐに反映されるようにするためです。

Vagrantを使っているので、まずこのフォルダ(プロジェクトフォルダ)をローカルのファイルシステムからVagrantのバーチャルマシンへと共有しなければなりません。同様にバーチャルマシンもフォルダをDockerコンテナへと共有します。もしこれをVirtualboxのデフォルトの共有フォルダ、そしてDisk IOでやると、結果的にRailsのパフォーマンスはひどいものになります。私の検証では、簡単なviewをレンダリングするのに20~30秒かかりました。

これはNFSによる共有を使うことで解決できます。NFSによる共有ははるかに早いですが、いくつかの追加設定とバーチャルマシン起動時にsudo passwordを入力する必要があります。Vagrantfile内で以下のように入力するとNFSが使用できます。

config.vm.synced_folder ".", "/app", type: "nfs"

NFS共有や設定のための必要項目は https://docs.vagrantup.com/v2/synced-folders/nfs.html を参照してください。OSXの場合はそのままで動作するはずです。Linuxの場合は nfsd をインストールする必要があるかもしれません。

Vagrant Up

新しい開発環境をスタートするためにvagrant upをプロジェクトのルート階層で実行してください。

sudo passwordを求められます。これはNFSの共有フォルダのために必要になります。

Virtualbox 4.3.10にあるバグのために、vagrant upを実行して初めて共有フォルダをマウントするときに以下の様なエラーに遭遇するかもしれません。

Failed to mount folders in Linux guest. This is usually because
the "vboxsf" file system is not available. Please verify that
the guest additions are properly installed in the guest and
can work properly. The command attempted was:

mount -t vboxsf -o uid=`id -u vagrant`,gid=`getent group vagrant | cut -d: -f3` vagrant /vagrant
mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` vagrant /vagrant

これは以下を実行することで解決できます。

vagrant ssh -c 'sudo ln -s /opt/VBoxGuestAdditions-4.3.10/lib/VBoxGuestAdditions /usr/lib/VBoxGuestAdditions'
vagrant reload

このコマンドにより、Ubuntuのバーチャルマシンを作成しその上にDockerをインストール、そしてVagrantfile内の$setup変数に定義されたスクリプトを実行するところまで進みます。今回の例では、完璧を求めるためにIndexからpullせずにすべてのコンテナをゼロから構築します。そのため初回実行時には長い時間がかかります。

セットアップスクリプト

バーチャルマシン初回起動時に、Vagrantfileの以下の部分が

config.vm.provision "shell", inline: $setup`

Vagrantfileの冒頭部分にある$setup変数に定義されたシェルスクリプトを実行します。

既存のシステムを再構築しないように、まずすべてのDockerコンテナを停止し削除することから始まります。

docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)

そしてDockerfilesからDockerイメージの構築に進み、以下のようにわかりやすい名前にタグ付けします。(それぞれに postgresrailsredisなど)

docker build -t postgres /app/docker/postgres
docker build -t rails /app
docker build -t redis /app/docker/redis/

Ubuntuバーチャルマシンの/appディレクトリは、あなたのRailsディレクトリのルート階層と相互に共有されているということを覚えておいてください。というわけでここではRailsプロジェクト内の他のファイルと同じように普通に閲覧、編集できるDockersfilesを使っています。
(原文:Remember that /app on the Ubuntu virtual machine is shared back to the root of your Rails directory, so this is using the Dockerfiles that can be viewed and edited as we normally would any file in a Rails project.)

このプロセスには長い時間がかかる可能性があります。OSXとVagrantとDockerの組み合わせで私が何度も経験したのは、なんらかの原因によるネットワーク切断により構築プロセスが永久にフリーズしてしまうことです。

もしそれが起こった場合、Vagrantのプロビジョナーを停止(ctrl+cを2回)させても何の害もありません。vagrant sshでログインして手動でコマンドを実行してください。

一旦この3つのイメージが構築されると、セットアップスクリプトがそれらのイメージからコンテナをスタートさせます。最初の2つはとてもシンプルです。

docker run -d --name postgres -e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker postgres:latest
docker run -d --name redis redis:latest

これらをひとつずつ説明していきます。

-d はバックグラウンドでの実行を意味します。

--name xyz はコンテナにxyzのようなわかりやすい名前をつけます。コンテナを停止したり、他コンテナにリンクする時に後からその名前で参照することができます。

-e は作成するコンテナ内の環境変数をセットすることを可能にします。今回は、PostgreSQLのユーザネームとパスワードを設定しています。Railsアプリからハードコーディングすることなしにこれらの機密情報にどうやってアクセスするかは、「リンクされたコンテナ内の環境変数」のセクションを参照してください。

xyz:latestxyzとタグ付けされた最新のイメージからコンテナを起動するという意味です。

私は、ひとつのDockerイメージはひとつのクラス定義である、という風に考えると有用だということに気づきました。ひとつのDockerfile(基本的には連続したシェルコマンド)とdocker buildコマンドを使ってイメージを作成します。

それからdocker runコマンドを使い、イメージからコンテナを作成します。コンテナはクラスのインスタンスのようです。ひとつのイメージから(docker runを実行するたびに新しい)複数のコンテナを作ることができます。それぞれのコンテナは、それらが同じ一つのイメージから作られているにも関わらず、他のコンテナとは完全に独立しています。

とは言うものの、他のコンテナの状態を基にしてイメージを作成するようなこともできます。ですのでクラスへの例え話にそこまで深入りするべきではないでしょう(!)

最後のdocker runは少しだけ複雑です。

docker run -d -p 3000:3000 -v /app:/app --link redis:redis --link postgres:db --name rails rails:latest

さっき説明したPostgresとRedisのための操作に加えて、

-p 3000:3000 は3000番ポートをホスト(Virtualbox VM)の3000番ポートとしてコンテナ向けに有効にします。Vagrantで、VMの3000番ポートをローカルマシンの3000番ポートに転送するように設定しているので、いつものRails開発サーバ(つまり http://localhost:3000)にアクセスするように、あなたの開発マシンの3000番ポートからこのコンテナへアクセスすることができます。

--link postgres:db は立ち上がったコンテナ(Railsアプリ)と先に立ち上げていたPostgresコンテナとのリンクを確立します。name:aliasというフォーマットで行われ、Postgresによって開放されたポートをRailsコンテナが使えるようにします。

リンクされたコンテナ内の環境変数

リンクすることで、Postgresコンテナの環境変数は ALIAS というプレフィクスを用いてRailsコンテナに対しても有効になります。コンテナ内のポートを開放した時、それに対応する環境変数がコンテナ内に作られます。

Postgresコンテナが5432番ポートを開放すると、それに対応したPORT_5432_TCP_ADDRという、PostgresコンテナのIPアドレスを含んだ環境変数へとつながっていきます。これはdatabase.ymlの中でPostgresコンテナに、そのIPの変更如何に関わりなく、自動で接続するために使っています。

Railsコンテナにおいて、このコンテナのエイリアスをdbとしたので、DB_PORT_5432_TCP_ADDRという、このコンテナのIPを含む環境変数を使います。

したがって、database.yml内のこの値にアクセスするために、ENV['DB_PORT_5432_TCP_ADDR']を使います。

Postgresイメージを作成するコマンドには以下も含まれています。

-e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker

これによりデータベースにアクセスするための情報も含めた形でdockerコンテナ内で環境変数がセットされます。これらはRailsコンテナではDB_ENV_POSTGRESQL_USERDB_POSTGRESQL_PASSとして有効になります(database.ymlを参照してください)。ALIAS_ENV_VARIABLE_NAMEというフォーマットに注意してください。

環境変数に関してのさらなる情報については、http://docs.docker.io/reference/run/#env-environment-variables を読むといいでしょう。また、環境が完全に立ち上がって動いた時点で、RailsコンソールからENVの中身を精査するのも興味深いです。

どうしてシェルスクリプトでやるのか?

$setupにある数行が、Dockerがインストールされたマシン上でアプリケーションを構築して動かすために必要なすべてです。もしLinode上でこのアプリケーションを開発のために動かしたいなら、新しいnodeを作り、Dockerをインストールし、コードをアップロードしてこの同じスクリプトを走らせれば、そのサーバ上にあなたのアプリケーションが動く開発環境を手に入れることができるでしょう。

これから公開される一連のチュートリアルで、このスクリプトのコマンドがDockerを使った本番環境デプロイの基礎を形作るためにどのように用いられていくのかを実際にやってみせます。日々の開発ワークフローの一部としてこれらのコマンドに親しんでおけば、本番の積み重ね(原文:working with production stack)でチーム内の開発者全員の習得にかかる時間を短くすることができます。

Railsアプリケーションとのやりとり

以上の設定が完了してしまえば、Railsアプリケーションがローカル開発マシンの http://localhost:3000 で確認できるはずです。しかしながら、まず最初にあなたが見るのは「データベースが存在しません」というエラーでしょう。

通常であれば、この段階で簡単にbundle exec rake db:create db:migrateを実行してデータベース作成、そしてマイグレーションを適用させることができます。今回はアプリケーションはDockerコンテナで動いているのでやり方が僅かに違います。

この設定では、各Dockerコンテナはひとつのプロセスを動かしています。$setupスクリプトでは以下のコードでRailsサーバのためのコンテナを立ち上げます。

docker run -d -p 3000:3000 -v /app:/app --link redis:redis --link postgres:db --name rails rails:latest

Dockerデーモンは'run'コマンドへの2つ目のデフォルト引数を待ちます。その引数はコンテナで実行されるコマンドになります。今回私たちはコンテナ内で実行されるコマンドを指定してないので、Dockerfileからデフォルトのコマンドが実行されます。これは /Dokcerfile内で以下のように指定されています。(原文:The Docker daemon expects the second, non-parametrised argument to the run command to be the command to be executed within the container. Since we don't specify a command to be run within the container, the default command from the Dockerfile is run. This is specified in /Dockerfile with:)

CMD["/start-server.sh"]

これは/start-server.sh内の以下のスクリプトが実行されるようにコンテナのデフォルトの動きを決めています。

#!/bin/bash
cd /app
bundle install
bundle exec unicorn -p 3000

これは以下のようにコンテナを起動するのと同じことです。

docker run -d -p 3000:3000 -v /app:/app --link redis:redis --link postgres:db --name rails rails:latest bash -c "cd /app && bundle install && bundle exec unicorn -p 3000"

Railsのイメージをベースにしたコンテナ内で他のコマンドを実行するには、docker runコマンドで同じ内容のコマンドを作り、Vagrantのバーチャルマシンの中から実行することができます(vagrant ssh)。bundle exec rake db:create db:migrateを実行するにはsshでVagrantホストに入り以下のコードを使います。

docker run -i -t -v /app:/app -link redis:redis --link postgres:db --rm rails:latest bash -c "cd /app && bundle exec rake db:create db:migrate"

これでRailsイメージをベースにした新しいコンテナが立ち上がり、db:createdb:migrateが行われます。追加されているコマンドラインフラグに注目してください。

-i -t はコンソールを標準入出力、エラーに紐付け、TTYをアサインしてそこで入出力できるようにします。これはbundle exec rails consoleのようなインタラクティブなコマンドを実行するときに必要になります。

--rm は実行が完了した時点でコンテナが削除されること意味します。

これを毎回やるのはやっかいです。この設定例ではこれを自動化するスクリプトを含んでいます。このファイルの最初の部分は、Railsプロジェクトのルートにある d です。例えば、

./d rc

とすると

vagrant ssh -c "sh /app/docker/scripts/rc.sh"

が呼び出され、Vagrantホストにsshで入って ./app/docker/scripts/rc.sh を実行することと同じ事になります。

スクリプト/app/docker/scripts/rc.shは以下のdocker runコマンドを含んでおり、

docker run -i -t -v /app:/app --link redis:redis --link postgres:db --rm rails :latest bash -c "cd /app && bundle exec rails c"

新しいコンテナでRails consoleを立ち上げます。

どの機能へのショートカットを持っているかを確認するために、引数なしで./d を実行することができます。ジェネリックなcmdオプションも提供されています。

./d cmd "bundle exec any_command"

これで新しいRailsコンテナの/appディレクトリ内で任意のコマンドを実行することができます。例えば、データベースを作成してマイグレートするためには以下のように実行します。

./d cmd "bundle exec rake db:create db:migrate"

これらはRailsコマンドに限定しません。/appディレクトリの中身を見るために

.d cmd "ls"

と実行することも同様にできます。

Railsアプリケーションとのこのようなやりとりを行うときは、すべてのコマンドが新しいコンテナで実行されており、ゆえに他のどのコマンドからも完全に独立している、ということを覚えておくことが重要です。したがって、あるコンテナで作られたローカルのリソースは通常他のコンテナからは参照できず、コマンドが完了してコンテナが削除された時点で消失します。

この開発設定時の例外として、共有されたフォルダである/app/ディレクトリがあります。このディレクトリで作成されたすべてのファイルは保持され、すべてのコンテナから参照することができます。本番環境の設定ではそういった共有されたローカルストレージのようなものはないことを気に留めておいてください。なので設定は共有された状態に依存させるべきではありません。共有状態へのアプローチの仕方については、12 Factor App guide http://12factor.net/ を読むといいでしょう。

データベースを自動実行

たいていの場合、私は開発環境をセットアップする時には開発用データベースに本番からダンプしたデータを入れておくことを好みます。それはまるごとのダンプであったり、本番環境のデータの一部分的であったり、取り扱いに注意すべき情報を削除するなどの変更を加えたデータであったりします。どちらにしろこれは通常.sqlファイルの形態になっています(MySQLやPostgresだとして)。将来的なダンプデータを開発用データベースに素早くリストアできることは非常に有用なことです。

./dの便利なスクリプトはそのためのシンプルなインターフェイスを提供します。

./d restore-db

これにより以下のことが行われます。

  • 開発用データベースのdrop, create, migrate
  • Railsのディレクトリ構造の中の db/current.sql.zip というzipファイルを探す
  • 新しいDockerコンテナ内でunzip
  • unzipされたファイルからcurrent.sqlというファイルを 'psql`ユーティリティを使ってインポート

名前に注意してください。current.sqlという名前のファイルが入っているzipが/db/current.sql.zipのように配置されている必要があります。

チュートリアルのこれから

これからの2~3ヶ月の間に以下のトピックでさらにチュートリアルをリリースします。更新情報については私のtwitter @talkingquickly をフォローして下さい。もしこのチュートリアルで問題を見つけたら気軽にツイートするか、私にメールしてください。 ben@talkingquickly.co.uk

  • 自分のDockerイメージを一元的に作成できるようにプライペートindexをセットアップ。新しい開発環境をスタートする時に、その都度再構築するのではなくすぐにpullできるようにする。
  • 本番環境用のイメージの作成
  • 本番用イメージ作成の自動化とプライベートindexへのpush
  • Capistranoとプライペートindexの組み合わせでの本番環境へのデプロイ

訳してみての感想

Ruby weekly http://rubyweekly.com/issues/198 で紹介されていたのを見かけて、苦手分野を克服してみるつもりで訳してみました。あまりデプロイ経験がないため怪しい部分あると思います。

  • どこまでそのままでいいのか?(pull/pushはいいとしてもindexとかイメージとかdropとかmigrateとか。。。)
  • start, build, run, execute, set upは何を操作するかで決まった言い方があるような気がしたけどその場の雰囲気でやってしまった感。
  • というか本番環境編がこのチュートリアルには入ってなかった。また訳す気になるかどうは自信なし。

間違いその他、「こういう言い方はしないよ」的なアドバイスなど貰えると嬉しいです。