前回の(1)はこちらから。
WebアプリケーションをDocker化するときの考え方
Dockerの基本がわかったところで、(2)では、WebアプリケーションをどのようにDocker化していくのか、なぜDocker化するのかについて説明します。
WebアプリケーションのDocker化の方針
Webアプリケーションを動作させるためには、中心となるWebアプリケーションサーバに加えて、データベースサーバ、KVS(Key-Value Store)サーバ、プロキシサーバなどが必要なことが多いでしょう。
まず、これらのサーバすべてをDocker上で動作させるという方針と、一部のサーバ、たとえばWebアプリケーションサーバのみをDocker化するという方針があります。次に、すべてのサーバを1つのDockerコンテナ内で動かすという方針と、各サーバをそれぞれ別のDockerコンテナ内で動かすという方針があります。さらに、開発環境、CI(Continuous Integration、継続的インテグレーション)環境、ステージング環境、本番環境のそれぞれの環境でDocker化する対象を限定するという方針があります。たとえば、本番環境ではWebアプリケーションサーバのみDocker化する、といったものです。
Dockerによる開発において、あらゆる状況に対応できるベストプラクティスは依然として確立されていないと筆者は考えます。そのような状況下で方針を決定するためには、Dockerにより解決したい課題を明確にする必要があります。
Dockerは非常に有用なツールです。しかし、Dockerを導入することにより、開発基盤のしくみは複雑になりがちです。解決したい課題が明らかでないと、Docker導入のメリットに対して割に合わないコストを支払うことになります。
Docker導入により解決したい課題
解決したい課題は状況により異なるため、一般化して話を進めるのは困難です。そこで、筆者が抱えている課題を例に話を進めていきたいと思います。筆者が抱えている課題は次のようなものです。
課題1:本番環境とローカル開発環境のOSが異なる
本番環境はLinuxサーバ上で動作しています。一方、ローカル開発環境では多くの開発者がOS Xを使用しています。現在のプロジェクトではLinux固有のシステムコールを用いるため、ローカル開発環境でも本番環境と同じLinuxを使いたいと考えています。
課題2:生成されるcpanfile.snapshotが異なる
CPANモジュールの管理にCartonを利用しています。しかし同じ内容のcpanfile
であっても、OS X上で生成するcpanfile.snapshot
と、Linux上で生成するcpanfile.snapshot
が異なることがあります。したがって、本番環境であるLinux環境に統一してcpanfile.snapshot
を生成したいと考えています。
課題3:プロジェクトごとにミドルウェアのバージョンが異なる
複数プロジェクトの開発を並行して進めています。しかし、各プロジェクトが依存しているPerl、MySQL、nginxなどのミドルウェアのバージョンが異なるため、ローカル開発環境においてプロジェクトごとにミドルウェアの環境を用意したいと考えています。
課題4:DevとOps間のコミュニケーションコストがある
開発環境はアプリケーションエンジニア(Dev)が、CI環境とステージング環境と本番環境はオペレーションエンジニア(Ops)が管理しています。そのような状況で、アプリケーションエンジニアがオペレーションエンジニアに対して必要なソフトウェアの追加を依頼するためのコミュニケーションコストをなくしたいと考えています。
課題の解決
これらの各課題について、先述した方針のうちどれを選ぶかを考えてみます。課題1については、ローカル開発環境でOS Xを使用していることが問題なので、ローカル開発環境のWebアプリケーションサーバのみをDocker化すれば解決できます。課題2については、課題1と同様にローカル開発環境で使用するOSの問題です。課題3については、ローカル開発環境でミドルウェアを含めてDocker化することにより解決できます。課題4については、すべての環境における差異が問題なので、すべての環境でミドルウェアを含めてDocker化する必要があります。
1つのコンテナに複数のWebアプリケーションやミドルウェアを詰め込むのか、1つのコンテナには1つのWebアプリケーションもしくはミドルウェアを割り当てるのかについては、基本的にDockerでは後者が想定されています。したがって、以降では後者を前提として話を進めます。
以上により、ローカル開発環境でWebアプリケーションサーバとミドルウェアをDocker化することで、課題1~3を解決できることがわかりました。本番環境でのDocker運用についてはまだまだこれからノウハウを蓄積していく段階であると考えているため、今回は課題4に関する議論は割愛します。次節以降では、Dockerを用いたローカル開発環境の構築に焦点を当てます。
PerlのWebアプリケーションをDocker化する方法
メインとなる本節では、PerlのWebアプリケーションをDocker化する方法について紹介します。
PerlがインストールされたDockerイメージの構築
まずは、PerlがインストールされたDockerイメージが必要です。Perl 5.20.2に加え、いずれのプロジェクトでも必要になるcpanminusとCartonをインストールするためのDockerfileの例をリスト2に示します。
リスト2では、ベースのイメージとしてdebian:wheezyを選んでいます。Debian GNU/Linuxは初期のイメージサイズが比較的小さいため、特別な理由がなければベースのイメージとして選択することをお勧めします。また、Perlのビルドにはperl-buildを用いています。
Dockerfileと同じディレクトリで次のコマンドを実行すると、Perl入りのDockerイメージを構築できます。
本当にPerlがインストールされているかを確認するために、コンテナを起動してみましょう。次のコマンドにより、先ほどのDockerイメージからコンテナを起動し、bashプロセスを立ち上げ、コンテナの中に入り、perl --version
を実行します。
-i
と-t
オプションにより、シェルによるインタラクティブな操作が可能になります。
PerlのWebアプリケーション向けのDockerfileの書き方
続いて、PerlのWebアプリケーション向けのDockerfileの書き方を紹介します。
Dockerfileの書き方は、プロジェクトのソースコードとCPANモジュールをDockerイメージ内に取り込むかどうかにより異なります。まずソースコードとCPANモジュールをイメージ内に取り込む方法を説明し、次にイメージ内に取り込まない方法を紹介します。ここでは、前者をBundled Container方式、後者をRuntime Container方式と呼ぶことにします[2]。
Bundled Container方式
Bundled Container方式のDockerfileをリスト3に示します。リスト2のDockerfileでビルドしたPerl入りのDockerイメージをFROMに指定しています。FROMによる継承を使わずに、リスト2に続けて、リスト3の内容を書いたDockerfileを用意してもよいでしょう。
リスト3ではまず、libmysqlclient-dev
などのCPANモジュールが依存するパッケージをインストールします。プロジェクト内のcpanfile
の内容によって、ここでインストールすべきパッケージは異なります。
次に、Webアプリケーションコードを配置するディレクトリを作成します。/code
ディレクトリ以下にWebアプリケーションコードが配置されるようにします。これ以降は/code
ディレクトリ以下で作業するため、WORKDIRにより、カレントディレクトリを変更します。
続いて、モジュールをインストールするためにcarton install
を実行します。ポイントは、carton install
の実行より先に、COPY
命令によりcpanfile
をDockerイメージ内に取り込んでいるところです。単純に考えれば、次のようにCOPY
命令でWebアプリケーションコードをすべて取り込み、carton install
を実行すれば済みます。
しかし、COPY
命令は取り込むディレクトリ以下の内容が変化するとビルドキャッシュが無効になり、COPY
命令以下の命令はスキップされません。上記ではWebアプリケーションコードを変更するたびにcarton install
が一から実行されるため、開発効率が低下します。そこでリスト3のように先にcpanfile
だけ取り込むことにより、cpanfile
が変更されたときのみ、carton install
を実行するようにします。これは「How to Skip Bundle Install When Deploying a Rails App to Docker if the Gemfile Hasn't Changed」で紹介されていたテクニックです。
最後に、CMD
命令によりplackup
を実行し、Webアプリケーションを起動します。CMD
命令の内容は、docker run
コマンドの引数により上書きできます。したがって、本番やステージングなどの環境に合わせて、ワーカ数を変化させて起動できます。
次のコマンドにより、リスト3のDockerfileをビルドし、コンテナを起動します。plackup
はデフォルトで5000番ポートをListenするので、-p 5000:5000
を指定して、コンテナの外から5000番ポートでアクセスできるようにします。
Runtime Container方式
Runtime Container方式では、WebアプリケーションコードとCPANモジュールをDockerイメージ内に持ちません。代わりに、DockerのData Volume機構によりホスト側のファイルシステム上のディレクトリをマウントして、コンテナ内から参照できるようにします。
リスト4にRuntime Container方式のDockerfileを示します。リスト3との違いは、COPY
命令によるWebアプリケーションコードの取り込みとcarton install
によるモジュールのインストールが省かれている点、そしてVOLUME
命令により/code
と/cpan
をData Volumeとして指定している点です。
まず、次のコマンドでリスト4のDockerfileをビルドします。
Runtime Container方式はあくまでPerlの実行環境のみを提供しています。リスト4のDockerfileをビルドしただけでは、carton install
は実行されません。
したがって、次のコマンドでコンテナを起動してcarton install
を実行する必要があります。docker run
には-v
オプションを付けて、ホスト側のディレクトリをマウントしています。さらに--rm
オプションを付けることで、コマンドの実行の終了と同時にコンテナを破棄します。
最後に、docker run
によりPlackサーバを起動します。
両方式の比較
Dockerの思想の一つに、ローカル開発環境で動作実績のあるコンテナをそのまま本番環境まで持っていくというものがあります。Bundled Container方式のメリットは、Dockerイメージ内にWebアプリケーションの動作に必要なものがすべて入っていることです。Bundled Container方式であれば、手元でビルドしたイメージをDocker HubやDistributionにpushして、本番環境からpullするというフローを作りやすいと思います。
しかしリスト3のDockerfileは、Webアプリケーションコードを変更するたびにdocker build
を実行しなければなりません。いくらcarton install
をスキップできるとはいえ、ローカル開発環境に導入するには少々面倒です。ほかにもGitのブランチごとにWebアプリケーションコードが異なるケースでのDockerイメージの管理などを考えると、どんどん新しい課題が出てきます。もともとやりたかったことはソフトウェアの依存関係の解決ですので、ローカル環境と本番環境で同じランタイム上でWebアプリケーションを実行できれば十分だと思います。
そこで、Runtime Container方式はOSとPerlの実行環境のみを提供することにより、変化の激しいWebアプリケーションコードとDockerイメージのビルドを分離できます。副次的なメリットとして、手元のファイルシステムにWebアプリケーションコードとモジュールがあるため、これまでの開発パラダイムを大きく変えないということがあります。現在のWebアプリケーション開発フローを考えると、Dockerに限らずなんらかの成果物のビルドをJenkinsやTravis CIのようなCIサーバに任せるのが自然です。ローカル開発環境からDocker Hubなどにpushせずに、CI環境でBundled Container方式でビルドしてpushするという方法もあると思います。
筆者は今のところ、特にDockerの導入初期には、パラダイムをあまり変えないRuntime Container方式を採用するのがよいと考えています。以降の説明では、Runtime Container方式の採用を前提とします。
<続きの(3)はこちら。>