Azure Webapps (Windows環境) に対する、JavaのWebアプリケーションのデプロイと自動化について(未完)

以下の記事が最高です。

[後悔しないための Azure App Service 設計パターン (2020 年版) - しばやん雑記](https://blog.shibayan.jp/entry/20200113/1578920798)

デプロイ周りの話もあるので、もはや私が書いた内容は不要となりました。読む価値もないです。

------

概要
------------------

Azure Webapps (Windows環境)について、Java製のWebアプリケーションをデプロイする方法について調べました。

が、しかし2018/05/22時点ではBestな方法はないようです。私が知らないだけで、本当はもっと良いやり方があるかもしれません。

※2018/10/4 追記: wardeploy APIを使った場合、ファイル名の大文字小文字を区別しないことによる不備がある模様です。辛い。

※2019/08/16 追記: サポートに問い合わせた結果、[Run From Package](https://docs.microsoft.com/ja-jp/azure/azure-functions/run-functions-from-deployment-package) を案内された。詳細は端折っていますが有力だと思います。

デプロイ手法の整理
----------------

[ドキュメント](https://docs.microsoft.com/en-us/azure/app-service/)に色々書いてありますが、正直、Javaアプリケーションではまともに使えるのは無いという判断です。


### FTP(S)によるwarファイルのアップロード

- ドキュメント:
    - [Deploy your app to Azure App Service using FTP/S](https://docs.microsoft.com/en-us/azure/app-service/app-service-deploy-ftp)
- 比較
    - :x: 起動中はwarファイルをプログラムが使用しているためか差し替えが出来ない。（tomcatのautoDeployが使えない）
    - :x: 差し替えをするには、該当のWebApps環境の停止が必要。（非停止で行う場合はデプロイメントスロットに対して行うべき）
    - :question: 未確認ですが、スケールアウト済みの環境に対して、うまくいかないかもしれません。（[kuduのドキュメント](https://github.com/projectkudu/kudu/wiki/Deploying-WAR-files-using-wardeploy)より）。        
    - :question: 未確認ですが、AutoDeployをfalseにすれば、warファイルの差し替えは出来るかも？（停止時間を減らせる？）




### API(wardeploy)を用いたwarファイルのアップロード

- ドキュメント:
    - [Deploy your app to Azure App Service with a ZIP or WAR file](https://docs.microsoft.com/en-us/azure/app-service/app-service-deploy-zip#deploy-war-file)
    - [Deploying WAR files using wardeploy](https://github.com/projectkudu/kudu/wiki/Deploying-WAR-files-using-wardeploy)
- 比較
    - :o: FTP(S)とは違い、プログラム使用中による差し替え不可が起きない。
    - :o: kuduで制御しているため、warの展開の競合がない
    - ~~:x: アップロードがFTP(S)と比べ、なぜか遅い。（2倍ぐらいかかる。）~~
    - ~~:x: warの差しかえではなく、warを展開し、差分をコピーする方式であり、**削除差分は反映しないため**、問題がある~~
    - :x: ファイル名の大文字小文字の変化を区別しない （2018/10/4 追記）
        - 例えばクラス名のリファクタリングを行った結果、`Test$Mycomparator.class`→`Test$MyComparator.class`となった場合です。大文字小文字違いのファイルが存在していると、ファイル名は置き換わらない。（結果、NoClassDefFoundError,ClassNotFoundExceptionになります）
        - 蛇足ですが、ファイルの中身は置き換わるので、ファイル名の大文字小文字を区別するような変更があった場合は要注意

~~上記の「差分をコピー」という挙動については1つ目のAzureのドキュメントには触れられてないですが、2つ目のkuduのドキュメントには「*How does wardeploy work*」にて明記されています。
問題点として、流行りのDIやTomcatの動作で、プロジェクト上は削除されたはずのクラスファイルがAzure Webapps環境上に残っていたせいで読み込まれてしまい、意図しない挙動をする可能性があります。実際、warに含まれないファイルが削除されない挙動を確認しています。これはあまりよくないです。~~

 (2019/08/16追記) アップロードの遅さは有りますが2倍程度ならまだ耐えれる範疇だと思ったので打消し線を入れています。~~憎しみだけで書いてしまった、ごめんなさい。~~SpringBootで2000のコントローラクラスを用意して1000クラスを削除して再デプロイしても、ちゃんと削除されていることが確認できたので、再現性無しということで打消し線を入れています。でも**大文字小文字の違いを区別しなかったので結局ダメ**です。（追記ここまで）

つまり、どれもダメそうです。もっとほかの方法を考えます。。。


### Run From PackageとAPI(zipdeploy)を使ったアップロード (2019/08/16追記)

FTPでやっていくぞ！と決めてしばらく離れて運用してもらっていたのだけど、50回に1回ぐらいはデプロイに失敗していたらしく、warの内容がダメだったかデプロイがこけたのか、悩まされていたらしい。

困ってサポートに投げたらRun From Packageを紹介された。でも、ドキュメントがAzure Functionsのページにあるの酷くない？


- ドキュメント:
    - [Azure Functions をパッケージ ファイルから実行する](https://docs.microsoft.com/ja-jp/azure/azure-functions/run-functions-from-deployment-package)
- 比較
    - :o: zipdeploy APIと一緒に使うことでcurlリクエスト1発でデプロイできる
    - :o: FTP(S)とは違い、プログラム使用中による差し替え不可が起きない（プロセスの再起動で賄われる模様）
    - :o: kuduで制御しているため、warの展開の競合がない
    - :o: ファイル名の大文字小文字の変化を区別しない問題が無い（wardeployでの問題は起きなかった）
    - :o: web.config次第で備え付けのTomcat利用ではなくExecutableなjarでも実行可能。つまり実行できればJavaじゃなくても使える。
    - :x: Run From Packageの構造に合うようにzipファイルを作る必要がある。（Linuxコマンドでunzip/zipが出来ればそんなに難しくない）
    - :x: 読み取り専用になるのでweb.config等が編集できなくなる（試行錯誤がやりにくい一方、web.configも一緒に管理できるので一長一短）

個人的には、FTPアップロード前の停止作業もいらず、かなりまともなデプロイ方法に思えます。
ただ、実運用に入れていないので安定性は不明・・・

（追記ここまで）




FTPSを使ったデプロイとJenkinsによる自動化
----------------

そうはいってもこれで開発を止めるわけにもいかなし、デプロイをいい感じにやってくれるような環境は欲しいです。

今回はJenkinsにFTPSによるアップロードで、デプロイメントスロット環境へデプロイさせます。swapは手動を想定しています。

社内のLinux(CentOS 6)の上でJenkinsが動いているものとします。

### サービスプリンシパルの作成

これはAzure CLIの例です。

```
az ad sp create-for-rbac --name webapp-deploy --password XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

こんな感じのレスポンスが得られます。覚えておきます。

```
{
  "appId": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY",
  "displayName": "webapp-deploy",
  "name": "http://webapp-deploy",
  "password": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "tenant": "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ"
}
```

### デプロイスクリプト

例えばJenkinsのジョブにこんな感じのスクリプトを仕込みます。

「FTPを使うとファイルの差し替えられない」という点ですが、一旦停止してからデプロイして起動すると一応warをいい感じに展開するし、もしリソースリークしてもごまかせるので、とりあえずこれで良いかなーとしました。

(2018/05/23 追記) [Azure Maven Pluginでも同じ方法を取っているようです](https://github.com/Microsoft/azure-maven-plugins/issues/37)

スケールアウト時はダメかもしれませんが、一応これで開発が回るのでしばらく動かして様子を見ます。

また、停止・起動に使うアクセストークンが有効期間1時間なんで、デプロイのたびに再取得します。面倒なので。

一応、curlとpythonを使うぐらいで他は特別なコマンドは使わないので、設定値さえ整えれば動くはず。

``` sh
### Azureのサービスプリンシパル情報

TENANT_ID=(サービスプリンシパル作成時のTenant)
CLIENT_ID=(サービスプリンシパル作成時のAppId)
CLIENT_SECRET=(サービスプリンシパル作成時のPassword)

### Webapps - デプロイメントスロット情報

SUBSCRIPTION_ID=(サブスクリプションID)
RESOURCE_GROUP_NAME=(リソースグループID)
WEBAPP_NAME=(webapp名)
SLOT_NAME=(slot名)

### WebApps - FTP設定

FTP_URL=(FTP URL)
FTP_USER=(FTP User)
FTP_PASS=(FTP Password)

# AppService上のrootから見たファイルのPath
FTP_FILEPATH=/site/wwwroot/webapps/ROOT.war

# JenkinsからAppServiceへアップロードするファイルのpath
UPLOAD_FILEPATH=(アップロードするwarファイルのpath)

##### 設定ここまで #####

# アクセストークンの取得

ACCESS_TOKEN=$(curl -s -X POST https://login.microsoftonline.com/${TENANT_ID}/oauth2/token -F grant_type=client_credentials -F resource=https://management.core.windows.net/ -F client_id=${CLIENT_ID} -F client_secret=${CLIENT_SECRET} | python -c 'import sys,json;print(json.load(sys.stdin).get("access_token"))')

# WebApps(slots)の停止

curl -X POST \
  --fail -s \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary "" \
  "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Web/sites/${WEBAPP_NAME}/slots/${SLOT_NAME}/stop?api-version=2016-08-01"


# WARをFTPでアップロード
curl \
  --fail -s --ftp-ssl \
  -T "${UPLOAD_FILEPATH}" \
  -u "${FTP_USER}"'\$'"${FTP_USER}:${FTP_PASS}" \
  "${FTP_URL}${FTP_FILEPATH}"

# WebApps(slots)の起動
curl -X POST \
  --fail -s \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary "" \
  "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Web/sites/${WEBAPP_NAME}/slots/${SLOT_NAME}/start?api-version=2016-08-01"
```




~~本当にこのサービスを本番利用している所あるんですか？居ないですよね？~~