App Service 上で稼働する、Tomcat アプリケーションへのデプロイ時に使用する Kudu API がよくわからない!
App Service に war ファイルをデプロイする API として /api/wardeploy
と /api/publish
の2種類があり、/api/wardeploy
のほうがおそらく歴史が古い。
2022年10月現在では、公式ドキュメントでは/api/publish
を使うように紹介されており、AZ CLI も内部的には、/api/publish
を使うようになっている。1
なお、/api/wardeploy
については、Kudu のリポジトリに WiKiページ が存在するが、/api/publish
については存在しない。
どちらを使うべきかわからない、違いがわからないといったことが多いので
本稿では実際に試して見えてきた内容を記載します。
TL;DR
-
/api/publish
を使いましょう -
/api/publish
を使うとネットワークドライブではなく、インスタンス固有のローカル領域にコピーされる。ファイル I/O のレイテンシの観点や、複数インスタンスの競合を避けるなどのメリットが有る - ここでいうローカルコピーは Java スタック独自の機能で、App Service の ローカルキャッシュとはべつもの
- ただし
ROOT
コンテキストとなる。 - ローカルコピーを有効にした状態で任意のコンテキストを用いたい場合は、スタートアップスクリプトで対応する必要あり
検証環境
OS: Windows
SKU: B1
Java: 11.0.13
Tomcat: 9.0.542
/api/wardeploy
と /api/publish
の違い
ここでの api 実行時には、純粋に Kudu の API の挙動を確認するため、WebApp としては停止状態で行っています。
WebApp が稼働状態の場合、API 実行をトリガーに、アプリケーションの再起動が行われ、初期化処理内でいろいろ行われる(後述)ためです。
/api/wardeploy
とは
/api/wardeploy
については、WiKi にあるとおり、warファイルの展開をTomcatに実行させずに予め展開した状態で %HOME%\site\wwwroot\webapps
(Linux の場合は$HOME/site/wwwroot/webapps
)に配置が行われる。
In simplest terms, it takes over the unpacking of the WAR file from Tomcat by unpacking it by itself. The WAR file is unpacked to a temporary location and the unpacked contents are then copied over to the appropriate directory in /home/site/wwwroot/webapps
この動作の目的としては、
%HOME%
あるいは$HOME
は、ネットワークドライブゆえに、複数インスタンスにスケールアウトした状態で、それぞれのインスタンスで動作する Tomcat が war を展開しようとする動作を防ぐことにある。
However, when you scale out the web app, multiple Tomcat instances race to unpack the WAR file leading to race conditions that may land one or more workers / instances in a bad state. This issue manifests in different ways, most common being a 404 status code when an incoming request is routed to any of the bad workers.
実行例
curl -X POST -u ${basicauth} "https://${myappName}.scm.azurewebsites.net/api/wardeploy" --data-binary @helloworld.war
dir /b /s
D:\home\site\wwwroot>dir /b /s
D:\home\site\wwwroot\webapps
D:\home\site\wwwroot\webapps\ROOT
D:\home\site\wwwroot\webapps\ROOT\META-INF
D:\home\site\wwwroot\webapps\ROOT\WEB-INF
D:\home\site\wwwroot\webapps\ROOT\index.jsp
D:\home\site\wwwroot\webapps\ROOT\META-INF\maven
省略
上記の通り、ROOT
コンテキストで展開されるため、helloworld.war 内の index.jsp は https://<appname>.azurewebsites.net/
でアクセスできるようになる。
実行例 name(context) 指定
API実行時に name
パラメータを指定することで、指定のコンテキストに展開することもできる
curl -X POST -u ${basicauth} "https://${myappName}.scm.azurewebsites.net/api/wardeploy?name=mycontext" --data-binary @helloworld.war
上記の通り、name
で指定した コンテキストで展開されるため、helloworld.war 内の index.jsp は https://<appname>.azurewebsites.net/mycontext/
でアクセスできるようになる。
wardeploy 利用時の懸念点
warファイルを事前に展開することで、Tomcat 起動時の複数インスタンスによる競合を避けるメリットはあるものの、あくまでも %HOME% 配下はネットワークドライブとなる。ネットワークドライブである限り、何かしらの接続エラーや、File I/O の遅延の発生の可能性は避けられない。
/api/publish
とは
/api/publish
については、公式ドキュメント上 Kudu publish API reference にあるとおり、デフォルトでは、warファイルは、app.war
として %HOME%\site\wwwroot
(Linuxの場合は $HOME/site/wwwroot
)に配置が行われる。
このとき、app.war
の展開は行われない。
- type=war: Deploy a WAR package. By default, the WAR package is deployed to /home/site/wwwroot/app.war. The target path can be specified with path.
実行例
※事前にwardeployを実行した際のファイルは削除した後に確認しています。
curl -X POST -u ${basicauth} "https://${myappName}.scm.azurewebsites.net/api/publish?type=war" --data-binary @helloworld.war
実行例 path(context) 指定
path を指定した場合は、そのディレクトリに展開されることになる。(wardeployでnameを指定した場合と同じ挙動)
curl -X POST -u ${basicauth} "https://${myappName}.scm.azurewebsites.net/api/publish?type=war&path=/home/site/wwwroot/webapps/mycontext" --data-binary @helloworld.war
D:\home\site\wwwroot>dir /b /s
D:\home\site\wwwroot\webapps
D:\home\site\wwwroot\webapps\mycontext
D:\home\site\wwwroot\webapps\mycontext\META-INF
D:\home\site\wwwroot\webapps\mycontext\WEB-INF
D:\home\site\wwwroot\webapps\mycontext\index.jsp
D:\home\site\wwwroot\webapps\mycontext\META-INF\maven
省略
/api/publish
で配置された app.war
はどうなるのか?
ここまで /api/wardeploy
と /api/publish
そのものの動作の違いを確認してきました。
/api/publish
を用いると、warファイルは展開されずに %HOME%\site\wwwroot\app.war
として配置されることがわかりました。
アプリケーション起動処理
%HOME%\site\wwwroot
以下に app.war
あるいは 展開済みの webapps/mycontext
ディレクトリが存在する場合に、アプリケーション起動時の動きを見ていきます。
App Service にて Java が言語スタックの場合、Windows 環境では w3wp.exe
のプロセスの配下に、JavaBootstrapper
、cmd
、java
とプロセスが存在することがわかります。
cmd
はプロセスの Handle からなんとなく catalina.bat
と考えられますが、その前段の JavaBootStrapper
は何をしているのかは残念ながら公開情報はありません。
そんなときは、Linux 版を見てみます。なにかヒントがあるかもしれません。
Linux 版の動作
Linux 環境で Java tomcat スタックのコンテナはどう動いているかを ps で見てみると java
の他には、bash /bin/init_container.sh
がいます。名前的に、コンテナのエントリポイントですね。
これを見てみると、ありました。なにやら app.war をコピーするぞって感じの分岐があります。
おそらく、JavaBootstrapper
も似たようなことをやっているはずです。
78 # This is the precedence:
79 # 1. /home/site/wwwroot/app.war
80 # 2. /home/site/wwwroot/webapps
81 # 3. Parking page app
82 # Note: If /home/site/wwwroot/webapps is used, we copy the app to a local location before using it, unless,
83 # - Local copy is turned on, in which case /home is pointing to a local share already (so local copy is unnecessary)
84 # OR
85 # - WEBSITE_SKIP_LOCAL_COPY is defined to 1 or TRUE.
86 if [ -f /home/site/wwwroot/app.war ]
87 then
88 SITEROOT=/usr/local/tomcat/webapps
89 cp /home/site/wwwroot/app.war $SITEROOT/ROOT.war
90 echo "Using app.war with SITEROOT=$SITEROOT"
91
92 # Enable WAR unpacking as we are sure only one app is present at this location
93 # So we don't have to worry about locking issues. On the other hand, this will provide some performance improvements.
94 TOMCAT_UNPACK_WARS=true
95 elif [ ! -d /home/site/wwwroot/webapps ]
96 then
97 SITEROOT=/usr/local/appservice/parkingpage
98 echo "Using parking page app with SITEROOT=$SITEROOT"
99
100 # Enable WAR unpacking as we are sure only one app is present at this location
101 # So we don't have to worry about locking issues. On the other hand, this will provide some performance improvements.
102 TOMCAT_UNPACK_WARS=true
103 elif [[ ! -z "$WEBSITE_LOCALCACHE_READY" || "$WEBSITE_SKIP_LOCAL_COPY" = "1" || "$WEBSITE_SKIP_LOCAL_COPY" = "true" ]]
104 then
105 SITEROOT=/home/site/wwwroot/webapps
106 echo "No local copy needed. SITEROOT=$SITEROOT"
107 else
108 SITEROOT=/usr/local/tomcat/webapps
109 cp -r /home/site/wwwroot/webapps/* $SITEROOT/
110 echo "Made a local copy of the app and using SITEROOT=$SITEROOT"
111 fi
catalina.log からの確認
%HOME%\site\wwwroot\app.war が配置されている場合(/api/publish
を使った場合)
-Dsite.appbase
が C:/DWASFiles/Sites/アプリ名/Temp/tomcatFiles
で始まるディレクトリが指定されていることがわかります。
すなわち Tomcat は、%HOME%
はもはや見ていません。
app.war は、ローカル領域(C:/DWASFiles/Sites/アプリ名/Temp/tomcatFiles
)にすでにコピーされているものと考えられます。
CATALINA_BASE: D:\Program Files\apache-tomcat-9.0.54
CATALINA_HOME: D:\Program Files\apache-tomcat-9.0.54
Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
Command line argument: -Djava.util.logging.config.file=D:\Program Files\apache-tomcat-9.0.54\conf\logging.properties
Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
Command line argument: -Dfile.encoding=UTF-8
Command line argument: -Dcatalina.valves.showReport=False
Command line argument: -Dcatalina.valves.showServerInfo=False
Command line argument: -Dsite.logdir=D:/home/LogFiles
Command line argument: -Dsite.home=C:/DWASFiles/Sites/<アプリ名>/Temp/tomcatFiles/845e1301-9b9c-4834-a578-6db4e85825da
Command line argument: -Dsite.libs=D:/home/site/libs
Command line argument: -Dsite.appbase=C:/DWASFiles/Sites/<アプリ名>/Temp/tomcatFiles/845e1301-9b9c-4834-a578-6db4e85825da/site/wwwroot/webapps
Command line argument: -Dsite.xmlbase=C:/DWASFiles/Sites/<アプリ名>/Temp/tomcatFiles/845e1301-9b9c-4834-a578-6db4e85825da/site/wwwroot
Command line argument: -Dsite.unpackwars=true
Command line argument: -Dsite.tempdir=D:\local\Temp
この、インスタンスのローカル領域にファイルをコピーしてくれる機能については、ローカルキャッシュのドキュメントにひっそりと書かれています。(なお、ローカルキャッシュとは別の機能)
https://learn.microsoft.com/en-us/azure/app-service/overview-local-cache
If you are using Java (Java SE, Tomcat, or JBoss EAP), then by default the Java artifacts--.jar, .war, and .ear files--are copied locally to the worker.
ローカルにコピーするメリットとしては、ネットワークストレージである%HOME%
($HOME
) に比べて、File I/O の向上や、予期せぬネットワークエラーの回避などが考えられます。
%HOME%\site\wwwroot\webapps\mycontext 以下に展開済みファイルが配置されている場合(/api/wardeploy
を利用した場合 、 /api/publish
でpathを指定した場合)
一方で従来の wardeploy によってすでに war ファイルが %HOME%\site\wwwroot\webapps 配下に展開されている場合、
-Dsite.appbase
が、Dsite.appbase=D:/home/site/wwwroot/webapps
を指しています。
Tomcat は、ネットワークドライブである、%HOME% を見ており、インスタンスのローカル領域にはアプリケーションファイルはコピーされていないという状況です。
CATALINA_BASE: D:\Program Files\apache-tomcat-9.0.54
CATALINA_HOME: D:\Program Files\apache-tomcat-9.0.54
Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
Command line argument: -Djava.util.logging.config.file=D:\Program Files\apache-tomcat-9.0.54\conf\logging.properties
Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
Command line argument: -Dcatalina.valves.showReport=False
Command line argument: -Dcatalina.valves.showServerInfo=False
Command line argument: -Dsite.logdir=D:/home/LogFiles
Command line argument: -Dsite.home=D:/home
Command line argument: -Dsite.libs=D:/home/site/libs
Command line argument: -Dsite.appbase=D:/home/site/wwwroot/webapps
Command line argument: -Dsite.xmlbase=D:/home/site/wwwroot
Command line argument: -Dsite.unpackwars=true
ローカル領域とはなんなのか
残念ながら Kudu UI の DebugConsole からでは、C:/DWASFiles/Sites/<アプリ名>/Temp/tomcatFiles/
や %LOCAL_EXPANDED%
の詳細は確認できません。(Linux 版の場合コピー先は /usr/local/tomcat
となっており、SSH 接続して自由に見ることができます)
そんなときは、スタートアップスクリプト startup.cmd
(or startup.ps1
) を利用することでわかります。
スタートアップスクリプトは 以下に説明がある通り、Tomcat が起動する前に(つまり、JavaBootStrapper によって) 実行されます。
For Windows sites, create a file named startup.cmd or startup.ps1 in the wwwroot directory. This will automatically be executed before the Tomcat server starts.
スタートアップスクリプトによる検証
AZURE_JAVA_APP_PATH
や AZURE_SITE_HOME
が使えそうです。
Setting name | Description |
---|---|
AZURE_JAVA_APP_PATH | Environment variable can be used by custom scripts So they have a location for app.jar after it's copied to loca |
AZURE_SITE_HOME | The value added to the Java args as -Dsite.home. The default is the value of HOME. |
下記の内容の startup.cmd
を %HOME%\site\wwwroot\
に配置してアプリケーションを起動してみます。
set CURRENT_DATE=%date:~10,4%%date:~4,2%%date:~7,2%
set logfile=%HOME%\LogFiles\startup_%CURRENT_DATE%.log
echo "==== startup.cmd start ===" >> "%logfile%" 2>&1
date /t >> "%logfile%" 2>&1
time /t >> "%logfile%" 2>&1
echo "AZURE_JAVA_APP_PATH = %AZURE_JAVA_APP_PATH%">> "%logfile%" 2>&1
echo "AZURE_SITE_HOME = %AZURE_SITE_HOME%">> "%logfile%" 2>&1
echo "dir %AZURE_SITE_HOME%\site\wwwroot\" >> "%logfile%" 2>&1
dir %AZURE_SITE_HOME%\site\wwwroot >> "%logfile%" 2>&1
echo "dir %AZURE_SITE_HOME%\site\wwwroot\webapps" >> "%logfile%" 2>&1
dir %AZURE_SITE_HOME%\site\wwwroot\webapps >> "%logfile%" 2>&1
echo "==== startup.cmd end ===" >> "%logfile%" 2>&1
残念ながら、%AZURE_JAVA_APP_PATH%
はドキュメントに記載の内容は取得できませんが(追記: JavaSE スタック Jarデプロイの場合に設定されます。 https://qiita.com/georgeOsdDev@github/items/436b21ffad30f7ca31d8 )、%AZURE_SITE_HOME%
が catalina.log に出力された C:/DWASFiles/Sites/<アプリ名>/Temp/tomcatFiles/<ランダム文字列>
に一致します。
そして、
C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d\site\wwwroot
配下に、
app.war
と ROOT.xml
と、空の webapps
ディレクトリが存在していることがわかります。
Tomcat起動前なので、app.war
はまだ展開されていませんね。
"==== startup.cmd start ==="
Fri 10/28/2022
01:41 PM
"AZURE_JAVA_APP_PATH = "
"AZURE_SITE_HOME = C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d"
"dir C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d\site\wwwroot\"
Volume in drive C is Temporary Storage
Volume Serial Number is 484F-9096
Directory of C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d\site\wwwroot
10/28/2022 01:41 PM <DIR> .
10/28/2022 01:41 PM <DIR> ..
10/28/2022 12:58 PM 6,771 app.war
10/28/2022 01:41 PM 156 ROOT.xml
10/28/2022 01:41 PM <DIR> webapps
2 File(s) 6,927 bytes
3 Dir(s) 11,725,500,416 bytes free
"dir C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d\site\wwwroot\webapps"
Volume in drive C is Temporary Storage
Volume Serial Number is 484F-9096
Directory of C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\211caf6a-7176-48f4-af7f-1b3d3a462d6d\site\wwwroot\webapps
10/28/2022 01:41 PM <DIR> .
10/28/2022 01:41 PM <DIR> ..
0 File(s) 0 bytes
2 Dir(s) 11,725,500,416 bytes free
"==== startup.cmd end ==="
ちなみに ROOT.xml は以下のような内容が記載されていました。
<Context displayName="app" docBase="C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\3c42584b-db11-45cf-acc9-3cf0c0ade9fa\site\wwwroot\app.war" path="" />
ローカルコピー機能は app.war(= ROOTコンテキスト) 以外は利用できるか
以下のように、startup.cmd の処理内で、app.war を 利用したいコンテキスト.war という名称で %AZURE_SITE_HOME%\site\wwwroot\webapps\
配下に移動することでOKでした。
試してないですが、app.war 以外にも、%HOME% のどこかに予め用意しておいた war ファイルを適宜%AZURE_SITE_HOME%\site\wwwroot\webapps\
以下にコピーすることで複数コンテキストを動かすことも可能と思われますが、1 WebAppに対しては 1 コンテキストが望ましい。3
set CURRENT_DATE=%date:~10,4%%date:~4,2%%date:~7,2%
set logfile=%HOME%\LogFiles\startup_%CURRENT_DATE%.log
echo "==== startup.cmd start ===" >> "%logfile%" 2>&1
date /t >> "%logfile%" 2>&1
time /t >> "%logfile%" 2>&1
echo "AZURE_JAVA_APP_PATH = %AZURE_JAVA_APP_PATH%">> "%logfile%" 2>&1
echo "AZURE_SITE_HOME = %AZURE_SITE_HOME%">> "%logfile%" 2>&1
echo "move %AZURE_SITE_HOME%\site\wwwroot\app.war %AZURE_SITE_HOME%\site\wwwroot\webapps\mycontext.war" >> "%logfile%" 2>&1
move %AZURE_SITE_HOME%\site\wwwroot\app.war %AZURE_SITE_HOME%\site\wwwroot\webapps\mycontext.war >> "%logfile%" 2>&1
echo "delete unused wwwroot\ROOT.xml" >> "%logfile%" 2>&1
del %AZURE_SITE_HOME%\site\wwwroot\ROOT.xml
echo "check %AZURE_SITE_HOME%\site\wwwroot" >> "%logfile%" 2>&1
dir %AZURE_SITE_HOME%\site\wwwroot >> "%logfile%" 2>&1
echo "check %AZURE_SITE_HOME%\site\wwwroot\webapps" >> "%logfile%" 2>&1
dir %AZURE_SITE_HOME%\site\wwwroot\webapps >> "%logfile%" 2>&1
echo "==== startup.cmd end ===" >> "%logfile%" 2>&1
[main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
[main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/9.0.54]
[main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\fdcceda6-132c-48ff-9ae2-090dfbb775c3\site\wwwroot\webapps\mycontext.war]
[main] com.microsoft.azure.appservice.logging.AppServiceLogger.info Log clean up task initializing. Log expiry period = 90 days
[main] com.microsoft.azure.appservice.logging.AppServiceLogger.info Log clean up task initialization succeeded
[main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [C:\DWASFiles\Sites\<アプリ名>\Temp\tomcatFiles\fdcceda6-132c-48ff-9ae2-090dfbb775c3\site\wwwroot\webapps\mycontext.war] has finished in [6,328] ms
[main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-127.0.0.1-24588"]
[main] org.apache.catalina.startup.Catalina.start Server startup in [8222] milliseconds
なお
-
az webapp deploy --debug
で確認可能 ↩ -
App Service で利用可能なTomcat 9.x 系の最新版(9.0.62)の1世代前 ↩
-
https://learn.microsoft.com/ja-jp/azure/developer/java/migration/migrate-tomcat-to-tomcat-app-service#create-and-deploy-web-apps ↩