Edited at

Tomcatを使ったWebアプリをDocker化する方法を検討

More than 3 years have passed since last update.

社内でDocker化を進めていますが、現在Tomcat上で動かしているJava製WebアプリをDocker化する方法を検討しています。

調べた限りでは何通りか方法があるのですが、本番稼働から開発環境までを通して期待する方法がなかったため、自分なりの方法を考えてみました。

まだ実践していませんが、こうすればできそうという方法ができたのでまとめてみました。

目標とした要件は以下の通りです。


  1. Tomcatで動作させること

  2. HTTPとAJPの両方に対応すること

  3. Dockerイメージのビルドがややこしくならないこと

  4. 開発環境もDockerで実行させること

  5. 開発時にコードの変更を簡易に素早く反映させられること

なるべく現状の動作方法を維持しつつ、開発環境にも対応させることを目標としています。


実現方法

早速、実現方法について紹介します。

紹介する方法はビルドにMaven、IDEにIntellij Ideaを使った環境で、それを前提とした部分もあります。

ですが、GradleやEclipseを使った場合でも多少の設定に気をつければ問題なく適用できると思います。


概要

想定するアプリケーションのフォルダ構成は以下になります。

├─ build.sh # Javaビルド用のスクリプト

├─ pom.xml
├─ Dockerfile
├─ docker-compose-dev.yml # 開発環境での起動用
├─ docker-compose.yml # 本番やStagingでの起動用
├─ springloaded-1.2.5.RELEASE.jar
├─ src
│ └─ main
│ ├─ java
│ │ ├─ Main.java
│ │ └─ controllers
│ │ └── HelloServlet.java
│ ├─ resources
│ │ ├─ config.properties
│ │ └─ logback.xml
│ └─ webapp
│ ├─ WEB-INF
│ │ └─ web.xml
│ ├─ images
│ │ └─ ・・・
│ └─ index.jsp
└─ target
├─ classes # Mavenでのビルド結果出力先
│ └─ ・・・・
└─ lib # `mvn dependency:copy-dependencies` タスクでjarをここに集める
└─ ・・・・

概要としては以下の通りです。


  • DockerイメージのベースはJavaイメージを使う

  • Embeded Tomcatを使う


    • Tomcatで使うclassesフォルダにmavenのビルド出力先を指定する (target/classes)

    • Tomcatで使うwebappsフォルダに src/main/webapps を指定する



  • 依存jarを mvn dependency:copy-dependencies タスクで target/lib フォルダにまとめる


    • その上でCLASSPATHにtarget/classestarget/lib/*を指定する



試しに作成した実装をGithubにあげていますので、詳細は以下を参考にしてください。

https://github.com/namutaka/docker-tomcat-app/


解説


Embeded Tomcatについて

Dockerではアプリの起動時設定を環境変数から指定するので、Tomcatの設定を動的に変更しやすくするためにEmbeded Tomcatを使っています。

また、任意の場所のclassファイルやwebappsフォルダを使うためでもあります。

Tomcatに関する処理は以下の Main クラスだけです。

https://github.com/namutaka/docker-tomcat-app/blob/master/src/main/java/Main.java

Tomcatのserver.xmlcontext.xmlでの設定をJavaコードで実装しています。server.xmlでのConnectorタグの設定に相当する処理にてHTTPとAJPのコネクタを登録しています。

ポイントとしては、以下の部分でアプリが使うclassesフォルダやwebappsフォルダに任意のパスを指定できるようにしています。


src/main/java/Main.java

private static Context addWebappContext(final Tomcat tomcat,

final String contextPath,
final String classesPath,
final String webappPath) throws ServletException {
// webapps フォルダの場所を指定
StandardContext ctx = (StandardContext) tomcat.addWebapp(
contextPath.replaceFirst("/$", ""), new File(webappPath).getAbsolutePath());
ctx.setUnpackWAR(false);

WebResourceRoot resources = new StandardRoot(ctx);
// classes フォルダの場所を指定
resources.addPreResources(new DirResourceSet(
resources, "/WEB-INF/classes",
new File(classesPath).getAbsolutePath(), "/"));

ctx.setResources(resources);
return ctx;
}


[2016.09.21 追記]

また、Tomcatの動作設定のほとんどは Connector#setProperty(key, value) メソッドでkey-valueの形式で指定するようになっています。

(参考: https://tomcat.apache.org/tomcat-8.0-doc/config/http.html)

そこで、指定した接頭語を持つシステムプロパティをそのままsetPropertyに設定するようにしてみました。

これで大体の設定はカバーできるのではないかと思います。

// Set attributes (https://tomcat.apache.org/tomcat-8.0-doc/config/http.html)

// Example:
// http.prop.connectionTimeout = 2000
// http.prop.bindOnInit = false
setPropertiesFromSystem(httpConnector, "http.prop.");

private static void setPropertiesFromSystem(Connector connector, String propKeyPrefix) {
for (String key : System.getProperties().stringPropertyNames()) {
if (key.startsWith(propKeyPrefix)) {
String propKey = key.substring(propKeyPrefix.length());
String propValue = System.getProperty(key);
if (IntrospectionUtils.setProperty(connector, propKey, propValue)) {
logger.info("{} set property {}={}", connector, propKey, propValue);
} else {
logger.warn("{} did not set property {}={}", connector, propKey, propValue);
}
}
}
}

Dockerといえば設定は環境変数で行うのが基本ですが、プロパティはkeyが大文字小文字を区別していそうな上に"."(ドット)が含まれるものがあるため、環境変数では表現しづらかったのでシステムプロパティを使いました。

docker-compose.yml では以下のように指定できます。

services:

web:
image: myapp
environment:
JAVA_OPTS: >
-Dhttp.prop.connectionTimeout=2000
-Dhttp.prop.acceptCount=20
-Dajp.prop.connectionTimeout=2000


Dockerfileについて

イメージをビルドするための Dockerfile は以下のとおりです。


Dockerfile

FROM java:8-jre-alpine

RUN mkdir /app
WORKDIR /app

COPY target/lib /app/target/lib
COPY target/classes /app/target/classes
COPY src/main/webapp /app/src/main/webapp

ENV CLASSPATH /app/target/classes:/app/target/lib/*
ENV JAVA_TOOL_OPTIONS -Djava.security.egd=file:/dev/./urandom

EXPOSE 8080 8009

CMD java $JAVA_OPTS Main


必要なフォルダをCOPYしてクラスパスを指定しているのがメインとなります。

Java起動時のオプションを変更できるように、JAVA_OPTSという環境変数で引数を追加できるようにしています。

また、起動コマンドを特別に指定したくなったときを想定して、コマンド引数を使わず環境変数を使って CMD をシンプルになるようにしています。

JAVA_TOOL_OPTIONSはコマンド引数の変わりに暗黙に利用される環境変数です。

これを使って SecureRandom で使われる乱数生成機を urandom にするためのシステムプロパティを指定しています。

Dockerコンテナの起動は人手による操作が入らないためなのかサーバー中に動作のブレが発生しづらいようで、この指定をしなければTomcatの起動がかなり遅くなることがあります。

この乱数生成機の事情について以下を参照ください。


起動方法

作成したDockerイメージの起動方法は以下のようにシンプルにコンテナを起動するだけです。


docker-compose.yml

version: '2'

services:
web:
image: myapp


一方、開発時の起動方法は以下のようになります。


docker-compose-dev.yml

version: '2'

services:
web:
build: .
volumes:
- ./target/lib:/app/target/lib:ro
- ./target/classes:/app/target/classes:ro
- ./src/main/webapp:/app/src/main/webapp:ro
- ./springloaded-1.2.5.RELEASE.jar:/app/springloaded-1.2.5.RELEASE.jar:ro
environment:
JAVA_OPTS: -javaagent:springloaded-1.2.5.RELEASE.jar -noverify


アプリで使用するフォルダをvolumeマウントして手元のファイルを使うようにしています。

また、Spring-Loadedを使うためのjarファイルもvolumeをつかってコンテナ内に入れています。

その上で、JAVA_OPTS環境変数にてSpring-Loadedを使うためのコマンド引数を設定しています。


使い方


開発環境の起動

開発環境では以下のようにビルドとサーバーの起動を行います。

$ mvn \

-DoutputDirectory=target/lib \
-Dcompile=compile \
clean \
dependency:copy-dependencies
$ mvn compile
$ docker-compose -f docker-compose-dev.yml up

このとき、webappsフォルダ内の静的ファイルやJSPファイルは修正すれば即座に反映されます。

また、Javaファイルは Spring-Loaded を使っているのでIDE上でコンパイルするだけで再起動もなく反映されます。

注意として Intellij のプロジェクト設定でビルドの出力先をMavenと同じにしておくことが必要です。これはMavenのプロジェクトをImportすれば自然とそうなるはずです。

クラスの追加やresourcesフォルダ内のファイルを変更した場合は、mvn compileをしてから docker-compose upを実行しなおす必要があります。

また、pom.xmlの依存モジールを修正した場合は最初の dependency:copy-dependencies からやり直しが必要です。

サンプルのプロジェクトでは、これらのビルドコマンドを build.sh ファイルにまとめています。


Dockerイメージのビルド

Dockerイメージをビルドする手順は以下の通りです。

$ mvn \

-DoutputDirectory=target/lib \
-Dcompile=compile \
clean \
dependency:copy-dependencies \
compile
$ docker build --tag myapp:1.0 .

このイメージの起動は以下のようになります。

$ docker-compose up


実装上のポイント

最後に現時点で私が思っているDocker化にあたっての注意点を記載しておきます。

Docker化にあたっては、Mavenのファイル置換機能や環境ごとのファイル差し替え機能は使わない方がよいと考えています。

特に今回の方法ではwebappsフォルダ内のファイルは src/java/webapps を直接利用するため対象とすることができません。

Dockerアプリの実装方針として挙げられる12factorsにおいても、1つのDockerイメージでどの環境向けにでも利用できるようにすべきとのことが謳われています。

これに従おうとするには、先のMavenのファイル操作機能は利用しない方がよいことになります。

ちなみに、このファイル操作機能を使わない場合はロガーモジュールとしてlogbackを使うのがおすすめです。

このロガーは設定ファイル(logback.xmlなど)で環境変数の値を直接利用することや、その値による条件判定(if文)をすることができます。

詳細は、以下を参照してください。

http://logback.qos.ch/manual/configuration_ja.html


まとめ

TomcatアプリのDocker化として開発環境での使い勝手を含めていい線の方法ができたのではないかと思います。

ただ、あくまでサンプルなので、これをベースに実際のDocker化を進めてみたいと思います。

なお、Tomcatを使うことを前提条件としていたのでこんな方法になっていますが、そもそもTomcat以外でもよければjettyで動作させる方が簡単です。

jettyでは、jetty-runnerを使うことでwarファイルを直接サーバ起動することができますし、開発環境向けにはjetty-maven-pluginを使うことでファイル更新を即座に反映させられる状態での起動ができます。

また、実際に試していませんがjettyでもAJPが使える(jetty9から対応外になったようなので今後は使えない?)ようですし、そもそもAJPを使わずHTTPだけでもそこまで問題があるわけではありません。

今回は、現状稼働させている環境をなるべく変えないことと、本番と開発のどちらもDockerを使ったほぼ同じ動作のさせ方をすることを前提とした、ある種の縛りプレイ的な検討になっています。


補足

おまけとして、既存で使えそうだった方法についての検討結果もまとめておきます。


Tomcat公式Dockerイメージを使う方法

DockerでTomcatを動かすのであれば、まず最初に選択肢に上がるのは公式のDockerイメージだと思います。

https://hub.docker.com/_/tomcat/

このイメージは、Tomcatの公式サイトでダウロードできるCoreのアーカイブを展開してそのまま動作させているもののようです。

これにはTomcatのmanagerやsampleアプリも入っているため、最初にそれらを削除する必要があります。

Tomcatのconf/server.xmlconf/context.xmlで定義される設定を変更するには、Dockerイメージのビルド時に自身のアプリに応じたファイルで上書きします。これらのファイルは${propname}という表記でシステムプロパティの値に展開できるようなので、javaコマンドの-Dオプションで動的に設定することもできます。

コンテナ内の /usr/local/tomcat/ にtomcatが展開されているので、この中の /webapps内にwarファイルか、warを展開したフォルダを配置することでアプリを実行できます。

なお、このTomcatはログをlogsディレクトリ下にファイル出力するようになっていますが、Dockerの場合は基本的にすべて標準出力に出す必要があるため、Tomcat本体のログ出力設定を変更する必要があります。

Mavenでは、mvn packageを実行することでtarget/{アプリ名}/が作成され、このフォルダを圧縮したtarget/{アプリ名}.warが作成されます。

開発中では、docker run時にこのtarget/{アプリ名}/フォルダをvolumeマウントすれば、mvn packageを実行することで再起動なく変更を反映できます。

しかし、毎回mvn packageを実行するのは手軽とは言いづらいです。

結論として、


  • 既存のmanagerやsampleを消す必要がある

  • 設定をファイルすげ替えで行う必要がある

  • 開発時の起動が手軽ではない

という点からこの方法は採用しませんでした。

ですが、もうちょっと工夫を検討すれば使いやすくできそうな気もしています。


Heroku の Tomcat webapp runnerを使う方法

HerokuのドキュメントにてTomcatアプリをHeroku上で実行する方法が紹介されていました。

これをDocker化にも使えるのではないかとも考えました。

ここではHeroku上でjavaアプリを実行するために、Tomcatそのものを1つのjarにまとめたWebapp Runnerというものをつかってwarファイルを直接起動する方法が紹介されています。

この Webapp RunnerはEmbeded Tomcatを使っ作成したモジュールのようで、以下で公開されています。

https://github.com/jsimone/webapp-runner

Tomcatをセットアップする必要がないので簡単ですが、開発時に利用するには先のTomcatイメージを使う方法と同じくファイル更新の反映にmvnコマンドの実行が必要となる問題があります。

なお、最初に紹介したEmbeded Tomcatを使ったMain.javaの実装はこのモジュールを参考にさせていただいています。


Heroku で解説されているEmbeded Tomcatを使う方法

Herokuのドキュメントでは、もう一つEmbeded Tomcatを使う方法も紹介されています。

これもHerokuでTomcatアプリ実行させるための方法ですがDockerにも応用できると思います。

Webapp Runnerを使わず、自前でWebapp RunnerでやっているようなTomcatの初期化を行う方法です。

この方法ではmvn packageを実行する必要がなく、Tomcatからは "src/main/webapp/" と "target/classes" を直接参照させるようになっています。

実行させるには、Mavenのappassembler-maven-pluginプラグインの機能で、アプリのjarファイルと実行用スクリプトを作成し、それを利用しています。

これは期待にかなり近い方法ですが、開発時の起動時にmvnコマンドの実行が必要となるためそのままでは不便さが残ります。

今回最初に紹介した方法は、これをベースに開発環境での起動方法を改良したものという感じになっています。


改版履歴

2016.09.21: Tomcatの動作設定方法について追記

2016.09.22: setPropertiesFromSystemのプロパティ設定をIntrospectionUtils.setProperty()に変更