社内でDocker化を進めていますが、現在Tomcat上で動かしているJava製WebアプリをDocker化する方法を検討しています。
調べた限りでは何通りか方法があるのですが、本番稼働から開発環境までを通して期待する方法がなかったため、自分なりの方法を考えてみました。
まだ実践していませんが、こうすればできそうという方法ができたのでまとめてみました。
目標とした要件は以下の通りです。
- Tomcatで動作させること
- HTTPとAJPの両方に対応すること
- Dockerイメージのビルドがややこしくならないこと
- 開発環境もDockerで実行させること
- 開発時にコードの変更を簡易に素早く反映させられること
なるべく現状の動作方法を維持しつつ、開発環境にも対応させることを目標としています。
実現方法
早速、実現方法について紹介します。
紹介する方法はビルドに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
を指定する
- Tomcatで使うclassesフォルダにmavenのビルド出力先を指定する (
- 依存jarを
mvn dependency:copy-dependencies
タスクでtarget/lib
フォルダにまとめる- その上でCLASSPATHに
target/classes
とtarget/lib/*
を指定する
- その上でCLASSPATHに
試しに作成した実装を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.xml
やcontext.xml
での設定をJavaコードで実装しています。server.xml
でのConnector
タグの設定に相当する処理にてHTTPとAJPのコネクタを登録しています。
ポイントとしては、以下の部分でアプリが使うclasses
フォルダやwebapps
フォルダに任意のパスを指定できるようにしています。
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
は以下のとおりです。
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の起動がかなり遅くなることがあります。
この乱数生成機の事情について以下を参照ください。
- Oracle への JDBC 接続に時間がかかる現象の回避方法 - ぐーたら書房
- インフラエンジニアのメモ : JAVAの処理遅延問題【乱数生成に伴うJVMの遅延】
起動方法
作成したDockerイメージの起動方法は以下のようにシンプルにコンテナを起動するだけです。
version: '2'
services:
web:
image: myapp
一方、開発時の起動方法は以下のようになります。
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.xml
やconf/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化にも使えるのではないかとも考えました。
- Deploying Tomcat-based Java Web Applications with Webapp Runner | Heroku Dev Center
ここでは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にも応用できると思います。
- Create a Java Web Application Using Embedded Tomcat | Heroku Dev Center
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()
に変更