Tomcatのwarファイルとデプロイと自動展開

またもやTomcatでハマってしまったので、ちょっとまとめておこうと思う。Tomcatでのwarファイルの扱いと、コンテキストパスと、自動デプロイについて。

Tomcatのデプロイとwarファイル

Tomcatではアプリケーションをデプロイするには、warファイルという、Webアプリケーションに必要なclassファイル・jarファイル・jspファイルやWEB-INF/xmlファイルなどをまとめてひとかたまりにしたファイルを配置してデプロイする。

このwarファイルは実質はただのzipファイルであるため、unzipコマンドで展開して中身を覗いてみることもできる。

$ file struts2-showcase.war
struts2-showcase.war: Zip archive data, at least v1.0 to extract
$ unzip struts2-showcase.war
Archive:  struts2-showcase.war
   creating: META-INF/
  inflating: META-INF/MANIFEST.MF    
   creating: actionchaining/
   creating: ajax/
(省略)

なおwarファイルは本来は、お作法的にはjarコマンドのxfオプションで展開するのが正しい(のだろう。たぶん)。しかしTomcatでは、warファイルを置くだけでデプロイできるので、基本的にこのファイルの中身を自前で展開する必要は無い。

$ jar xvf struts2-showcase.war

ちなみにwarファイルとは、「Web Application aRchiveファイル」の略である。……と、ここまでが予備知識。

では以下にまず、Tomcatのデプロイの仕組みをおさらいする。非常にややこしい。

warファイルの展開とデプロイ

一般的には、.warファイルをappBaseディレクトリに設置する(要するに、単にファイルコピーすれば良い)と、warファイルがTomcatにより自動的に展開されて、その名前のディレクトリができる。これがWebアプリケーションのコンテキストパスとなる。

例えばmySampleApp.warをappBaseディレクトリ(普通は$TOMCAT_HOME/webappsですね)に置くと、mySampleApp.warファイルが展開されて[mySampleApp]というディレクトリができる。ファイルの展開は、Tomcatが勝手にやってくれる。

webapps/mySampleApp/
webapps/mySampleApp.war

そしてTomcatはデプロイ元として、warファイルではなくコンテキストパスの方(mySampleApp/ディレクトリ)から読み込んでWebアプリケーションを実行する。

だからディレクトリが展開された状態でデプロイ済みのアプリのxml設定内容を変えるときなどは、展開されたディレクトリ内のxmlファイルを書き換えれば、warファイルはそのまま元のままにしておいても設定変更は反映される。「ひょっとしてwarファイルの中のxmlを優先して読んじゃって、設定が反映されないのでは……」という心配は無用である。ただし、このパターンではxmlを読み直すために、Tomcatの再起動は必要である。

unpackWARsとautoDeploy

今までTomcatではwarファイルを置くだけで良いと書いたが、その鍵がconf/server.xmlで定義されている二つのパラメタ、unpackWARsとautoDeployである。どちらもデフォルトではtrueに設定されている。

<Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true">

また、隠し(?)パラメータとしてdeployOnStartupがある。これもデフォルトではtrueである。

Tomcatでは、warファイルが勝手に展開されることと、デプロイされることは、微妙に違うステージになっている。そのためこれを混同するとハマることがあるので気をつけよう。以下にそれを説明する。

unpackWARs

これは書いてあるとおり、自動的にwarファイルを展開してくれるかどうかを設定する。これをもしfalseにすると、アプリケーションはwarファイルのまま動作する。例えばmySampleApp.warを置くと、[mySampleApp]というディレクトリは作られず、mySampleApp.warを直接Tomcatが読み込んでデプロイする。

そのため、warファイルとディレクトリ展開されたもののバージョンが食い違ってしまうことを嫌って、あえてunpackWARsをfalseにする運用も一部にあるらしい。falseにした際のデメリットは、ちょっと設定値を確認したいと思ってxmlファイルを見たくても、いちいちwarのコピーを取ってどこかで展開して中身を見ないと分からないことである。これは結構ストレスである。

ちなみに、Tomcatハンドブックを見る限りは、「ほとんどの場合は、warファイルは展開して使った方がいいよ」というノリで書いてある。

autoDeploy

これはその名の通り、Tomcat動作中に自動的にアプリケーションをデプロイしてくれるようにする設定である。Tomcatの再起動無しにWebアプリケーションをバージョンアップするためには、これがtrueでないといけない。

動作としては、Tomcatが、appBaseディレクトリ(普通は$TOMCAT_HOME/webapps)にあるwarファイルを常に監視していて、

  • タイムスタンプに更新があったら再デプロイ
  • 新規のwarファイルが置かれたら新しいアプリケーションとしてデプロイ

してくれるというものである。この際、unpackWARsがtrueならば、ディレクトリ展開も同時に行われる。

そのため、もしwarファイルが展開された後に、ディレクトリ内でxmlの設定値を一部手動で変えており、その後にそのことを忘れてwarファイルを新しくしてしまうと設定値も消し飛んでしまう。これには注意が必要である(何度かハマった)。

autoDeployは定期的にappBaseディレクトリを監視しているため、trueにすると若干パフォーマンスが落ちる。また、誤ってwarファイルに何か操作してしまった時に勝手にデプロイされてしまうことを嫌って、敢えてfalseにする運用もある(が、私は不便すぎるのでtrueしか使ったことはない)。

なお、Tomcatが停止している状態から起動した場合(クリーンブート)には、このautoDeployは走らないで通常のデプロイがされる。何を言ってるのかというと……それを次に述べる。

よくハマるパターン

今、以下のような構成でTomcatが動いているとする。

server.xml: 
<Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true">

appBaseディレクトリ内:
webapps/mySampleApp/
webapps/mySampleApp.war

ここで、以下のようなオペレーションでmySampleAppをバージョンアップしようとすると、新しいバージョンのwarで正しくデプロイしたつもりになっていても、実は旧版が動き続けてしまうというトラップに引っかかる。

  1. Tomcatを停止する
  2. mySampleApp.warを新しいバージョンのもので上書きする
  3. Tomcatを起動する
  4. catalina.outでmySampleAppのデプロイ(配備)を確認する

この場合には、古い[mySampleApp]ディレクトリがあるため、unpackWARsは走らない。そしてTomcatディレクトリとwarではディレクトリの方を優先するため、結果として古いバージョンが入っている[mySampleApp]ディレクトリがデプロイされてしまい、旧版が動いてしまうことになる。

なお、Tomcatを起動してしまった後でこの状態を直すには、以下の2つの方法がある。

  • Tomcatが起動している状態で、mySampleApp.warのタイムスタンプをtouchコマンドで最新にする。するとwarファイルが更新されたとみなされautoDeployが走り、同時にunpackWARsも動いて[mySampleApp]ディレクトリも最新になる。
  • Tomcatが停止していても起動していてもいいから、[mySampleApp]ディレクトリをいきなりrm -rfで削除してしまう。すると、停止している場合は起動時にディレクトリが無いために自動的にunpackWARしてくれるし、稼働中ならばコンテキストに変更ありとみなされてautoDeployが走り、自動的にunpackWARされる。

2つめの、ディレクトリを消してしまうというのはかなり強引に見えるが、もしautoDeployをfalseにした運用をしている場合は、warファイルへのtouch作戦が使えないため、こちらの手法を使うしかない。(つまり手順としては、Tomcatを停止して、warを入れ替えて、ディレクトリを削除して、Tomcatを起動する)。

正しいデプロイ方法

こういうのを書くとなんかデプロイ職人っぽくて、最近の継続的インテグレーションとかの流れに反していてアレだけど……。要するに、必要に応じて古いコンテキストのディレクトリを削除してしまう、というのが重要である。

autoDeploy="true"で、Tomcatを再起動したくないとき
  1. warファイルを新しいもので上書きする。
  2. catalina.outでデプロイ(配備)ログを確認する。
  3. 展開ディレクトリの中を確認し、タイムスタンプが新しいものであることを確認する。
autoDeploy="true"で、Tomcatを再起動するとき

メモリリークを嫌って、メンテナンスの際にTomcatの再起動も一緒にしたいことがある。

  1. Tomcatを停止する
  2. 古いコンテキストパスのディレクトリを削除する。(これを忘れやすい)
  3. warファイルを新しいもので上書きする。
  4. Tomcatを起動する。
  5. catalina.outでデプロイ(配備)ログを確認する。
autoDeploy="false"のとき

この場合は当然のことながらTomcatの再起動は必須である。手順は、上記の「autoDeploy="true"で、Tomcatを再起動するとき」と全く同じ。

  1. Tomcatを停止する
  2. 古いコンテキストパスのディレクトリを削除する。(これを忘れやすい)
  3. warファイルを新しいもので上書きする。
  4. Tomcatを起動する。
  5. catalina.outでデプロイ(配備)ログを確認する。