WARデプロイの実際と並行デプロイ(Parallel Deployment)
一言でいうと
WARをただコピーするデプロイには、展開中の404という穴があり、Tomcatはすでにその穴を埋める手段(##버전の並行デプロイ。韓国語の語は「バージョン」を意味します)を持っているのに、大半の現場がそれを知りません。
なぜこれが問題なのか
停止デプロイは安全ですが、その間サービスが止まります。無停止にしようとWARだけを上書きすると、展開が終わるまで、そのパスは404で、この時間はクラスが多いほど長くなり、30秒を超えることもあります。短いからと見過ごせない理由は、その30秒がサービスイン初日の午前9時にかかるからです。
しかも、失敗が静かです。コピーの途中でTomcatが展開を始めて、半分のWARを展開しようとして失敗すると、ログには残りますが、デプロイスクリプトの終了コードは0です。デプロイした人は、成功したと信じて帰宅します。
WARはどのようにデプロイされるのか
webapps/order.warをコピーすると、Tomcatがwebapps/order/に展開して、/orderパスでサービスします。これがautoDeploy="true"の動作です。便利ですが、運用には落とし穴があります。
- コピーの途中で、Tomcatが展開を始めます。大きなWARをネットワーク越しにコピーすると、半分しかないファイルを展開しようとして失敗します。そのため、デプロイスクリプトは、別の名前でコピーしたあと、
mvでアトミックに名前を変更します。 webapps/order/ディレクトリが先に削除されます。その瞬間から、新しいアプリが立ち上がるまで、/orderは404です。短ければ3秒、クラスが多ければ30秒以上です。- 削除の失敗。開いているファイルハンドルのせいで、展開ディレクトリが完全に削除されず、古いクラスが残ることがあります。そのため、デプロイ手順書に「Tomcat停止 → ディレクトリ削除 → work削除 → WARコピー → 起動」が入ります。安全ですが、その間サービスが止まります。
並行デプロイ: Tomcatがもともと持っている無停止の手段
Tomcatには、あまり知られていない機能があります。ファイル名に##버전を付けると、同じコンテキストパスに複数のバージョンを同時に載せられます(プレースホルダーはバージョンです)。
webapps/order##001.war ← 기존 버전
webapps/order##002.war ← 새 버전
この状態で、Tomcatは次のように動作します。
- 新しいリクエスト(セッションなし)は、最も高いバージョンである
##002に行きます。 - 既存のセッションを持つリクエストは、引き続き
##001に行きます。 ##001のセッションがすべて期限切れになったら、そのとき##001を停止しても、誰も影響を受けません。
つまり、セッションを切らずに新しいバージョンに移れます。L4の前段でサーバーを外したり入れたりする従来の無停止デプロイができない単一WAS環境で、これはかなり使えるカードです。
バージョン文字列は文字列の比較で並べられるので、##1、##2、##10のように書くと、##10が##2より前に来ます。##001のように0を埋めるか、##2026-08-19_1430のようなタイムスタンプを使います。
それでも停止デプロイをする場合
並行デプロイは万能ではありません。次の場合は使えません。
- DBスキーマが変わった場合: 2つのバージョンが同じDBを見ているのにスキーマが違うと、どちらかが壊れます。そのため、スキーマ変更は、拡張(expand) → 移行 → 縮小(contract)の3段階に分け、1回のデプロイに2つの段階を入れません。
- 静的リソースのキャッシュが混乱する場合: 2つのバージョンが、同じURLで違うJSを返します。
- グローバルな状態を使う場合: シングルトンのキャッシュ、スケジューラー、ファイルロック。
そのため、実務の判断は次のとおりです。 画面・ロジックだけが変わるなら並行デプロイ、スキーマ・バッチ・グローバルな状態が絡むなら停止デプロイ。
WARと実行可能JARは何が違うのか
最近の新規開発はSpring Bootのfat jarで行い、既存のシステムはWARです。同じ組織の中に両方が共存するのはよくある状況なので、違いを整理しておくとよいです。
| 項目 | WAR+外部Tomcat | 実行可能JAR(組み込みTomcat) |
|---|---|---|
| デプロイ単位 | WARファイル | JARファイル |
| Tomcatのバージョン | サーバー管理者が管理(複数のアプリが共有) | アプリが保持(アプリごとに異なりうる) |
| ポート | server.xml | 実行引数 / プロパティ |
| 複数アプリの配置 | 1つのWASに複数のコンテキスト | プロセスを複数立ち上げる |
| ロールバック | 以前のWARに置き換え | 以前のJARにプロセスを置き換え |
| 無停止 | 並行デプロイまたはL4 | プロセスの置き換え+LB |
| 運用標準 | WASを基準とした手順書 | プロセス/コンテナを基準とした手順書 |
運用組織の標準手順書が、どちら側を前提に書かれているかが、実際の選択を左右します。技術的にはJARが楽でも、WAS単位で監視・起動・バックアップの手順が標準化されている組織では、JAR1つのために手順を新しく作る必要があります。そのコストが、技術的な利点より大きいことがあります。
デプロイスクリプトが備えるべき最低限
現場で使えるデプロイスクリプトは、これくらいのことをします。
- 引数の検証: WARファイルが実際に存在し、0バイトでないか
- バックアップ: 現在デプロイされているものに、タイムスタンプを付けて保管
- アトミックな配置: 一時的な名前でコピーしてから
mv - 展開の待機: 最大N秒間、コンテキストが応答するまでポーリング
- 検証: ヘルスチェックURLが200で、バージョン文字列が期待値か
- 失敗したら即中止: 5つ目が失敗したら、デプロイ成功として報告しない
4つ目の「展開の待機」が特に重要です。cpの直後にcurlすると、まだ404です。それを失敗と判定してロールバックするスクリプトも見ましたが、逆に、待機なしで「デプロイ完了」を出力して終わるスクリプトは、はるかに多く見ました。後者は、失敗したデプロイを成功と報告します。はるかに悪いです。
現場での姿
そのため、デプロイスクリプトが備えるべき最低限が決まっています。別の名前でコピーしてから、mvでアトミックに名前を変更し、展開が終わるまで待ってから、実際の応答が200であることを確認してから終了コードを出します。コピー直後にすぐ確認すると、まだ展開中なので失敗しますが、これを「デプロイ失敗」と誤解して元に戻すことも、よくあります。