Perl Hackers Hub

第34回DockerによるPerlのWebアプリケーション開発(2)

前回の(1)こちらから。

WebアプリケーションをDocker化するときの考え方

Dockerの基本がわかったところで、⁠2)では、WebアプリケーションをどのようにDocker化していくのか、なぜDocker化するのかについて説明します。

WebアプリケーションのDocker化の方針

Webアプリケーションを動作させるためには、中心となるWebアプリケーションサーバに加えて、データベースサーバ、KVSKey-Value Storeサーバ、プロキシサーバなどが必要なことが多いでしょう。

まず、これらのサーバすべてをDocker上で動作させるという方針と、一部のサーバ、たとえばWebアプリケーションサーバのみをDocker化するという方針があります。次に、すべてのサーバを1つのDockerコンテナ内で動かすという方針と、各サーバをそれぞれ別のDockerコンテナ内で動かすという方針があります。さらに、開発環境、CIContinuous 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 Perl用のDockerfile
FROM debian:wheezy

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update -yq && \
    apt-get install -yq --no-install-recommends \
    build-essential \
    curl \
    ca-certificates \
    tar \
    bzip2 \
    patch && \
    apt-get clean && \
    rm -rf /var/cache/apt/archives/* && \
    rm -rf /var/lib/apt/lists/*

ENV PERL_VERSION 5.20.2
ENV PATH /opt/perl-$PERL_VERSION/bin:$PATH
ENV PERL_CARTON_PATH /cpan

RUN curl -sL http://git.io/perl-build > \
    /usr/bin/perl-build
RUN chmod +x /usr/bin/perl-build
RUN perl-build $PERL_VERSION /opt/perl-$PERL_VERSION
RUN curl -sL http://cpanmin.us/ | \
    /opt/perl-$PERL_VERSION/bin/perl - \
    --notest App::cpanminus Carton

リスト2では、ベースのイメージとしてdebian:wheezyを選んでいます。Debian GNU/Linuxは初期のイメージサイズが比較的小さいため、特別な理由がなければベースのイメージとして選択することをお勧めします。また、Perlのビルドにはperl-buildを用いています。

Dockerfileと同じディレクトリで次のコマンドを実行すると、Perl入りのDockerイメージを構築できます。

Perlのイメージのビルド
$ docker build -t perl-5.20.2 .

本当にPerlがインストールされているかを確認するために、コンテナを起動してみましょう。次のコマンドにより、先ほどのDockerイメージからコンテナを起動し、bashプロセスを立ち上げ、コンテナの中に入り、perl --versionを実行します。

Perlのイメージ内に入る
$ docker run --rm -i -t perl-5.20.2 /bin/bash
root@0bf912ff086d:/# 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 Bundled Container方式のDockerfile
FROM perl-5.20.2

RUN apt-get update && \
    apt-get install -yqq --no-install-recommends \
    mysql-client-5.5 \
    libmysqlclient-dev \
    libssl-dev && \
    apt-get clean && \
    rm -rf /var/cache/apt/archives/* && \
    rm -rf /var/lib/apt/lists/*

ENV APPROOT /code
RUN mkdir -p $APPROOT
WORKDIR /code

COPY cpanfile $APPROOT/cpanfile
RUN carton install
COPY ./ $APPROOT

EXPOSE 5000
CMD ["carton","exec","plackup","-a","script/local-server"]

リスト3ではまず、libmysqlclient-devなどのCPANモジュールが依存するパッケージをインストールします。プロジェクト内のcpanfileの内容によって、ここでインストールすべきパッケージは異なります。

次に、Webアプリケーションコードを配置するディレクトリを作成します。/codeディレクトリ以下にWebアプリケーションコードが配置されるようにします。これ以降は/codeディレクトリ以下で作業するため、WORKDIRにより、カレントディレクトリを変更します。

続いて、モジュールをインストールするためにcarton installを実行します。ポイントは、carton installの実行より先に、COPY命令によりcpanfileをDockerイメージ内に取り込んでいるところです。単純に考えれば、次のようにCOPY命令でWebアプリケーションコードをすべて取り込み、carton installを実行すれば済みます。

COPY ./ $APPROOT
RUN 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番ポートでアクセスできるようにします。

リスト3のDockerfileのビルド
$ docker build -t docker-sample .
Dockerコンテナの起動
$ docker run -d -p 5000:5000 docker-sample

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 Runtime Container方式のDockerfile
FROM perl-5.20.2

RUN apt-get update && \
    apt-get install -yqq --no-install-recommends \
    mysql-client-5.5 \
    libmysqlclient-dev \
    libssl-dev && \
    apt-get clean && \
    rm -rf /var/cache/apt/archives/* && \
    rm -rf /var/lib/apt/lists/*

RUN mkdir -p /code

EXPOSE 5000
VOLUME ["/code", "/cpan"]
WORKDIR /code

まず、次のコマンドでリスト4のDockerfileをビルドします。

リスト4のDockerfileのビルド
$ docker build -t docker-sample-runtime .

Runtime Container方式はあくまでPerlの実行環境のみを提供しています。リスト4のDockerfileをビルドしただけでは、carton installは実行されません。

したがって、次のコマンドでコンテナを起動してcarton installを実行する必要があります。docker runには-vオプションを付けて、ホスト側のディレクトリをマウントしています。さらに--rmオプションを付けることで、コマンドの実行の終了と同時にコンテナを破棄します。

リスト4のDockerfileのビルド
$ docker run --rm -v ./:/code -v ./local:/cpan docker-samp le-runtime carton install

最後に、docker runによりPlackサーバを起動します。

Dockerコンテナの起動
$ docker run -d -p 5000:5000 -v ./:/code -v ./local:/cpan docker-sample-runtime carton exec plackup -a script/localserver

両方式の比較

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)こちら。>

おすすめ記事

記事・ニュース一覧