Java on Azure 実践開発【App Service デプロイ用スクリプトを利用したサーバーサイドビルド】

## はじめに

ポリシー上、GitHubを使えない企業様は多いのではないでしょうか。また、ソースコードを外部に配置することになんとなく嫌な気持ちになるエンジニアの方もいらっしゃるのではないでしょうか。
かといって、ローカルでビルドサーバーをもつのも、管理が大変ですよね。「App Service をホストしておくだけで、デプロイまで完結できればいいのに」なんて思っている方も少なくないのではないでしょうか。

そんな皆様に、本記事では、App Service をホストしておくだけで、デプロイまで完結させる方法をご紹介します。

## App Service

Azure App Service とは、Web アプリケーションやREST API、モバイルバックエンドをホストするための PaaS サービスです。

公式サイト: 『[App Service](https://azure.microsoft.com/ja-jp/services/app-service/)』

## App Service へのデプロイ方法

Azure App Service には、色々なデプロイ方法があります。

1. ローカルコンピュータ上の Git リポジトリからアプリケーションをデプロイする
2. CI/CD で Git のリモートリポジトリを使用してデプロイする
3. Zip デプロイ
4. GitHub Actions や Azure Pipelines 等 CI/CD パイプラインからのデプロイ
4. Run From Package（ビルド済み結果を App Service からマウントし、ドキュメントルートとする方法）

上記のうち、1～3のデプロイ方法では、デプロイ用のスクリプト（Deployment Script）が自動生成され、これが実行されることでデプロイを実現しています。

本記事では、このデプロイ用スクリプトをカスタマイズし、アプリケーションのサーバーサイドビルドを実行します。

## デプロイ用スクリプトのカスタマイズ

App Service でデプロイ用スクリプトをカスタマイズする方法は、以下の Docs に公開されています。ただ、**Java アプリケーションへ適用する方法は公開されていませんでした。**

- Docs: 『[Azure App Service への継続的デプロイ - リポジトリを準備する](https://docs.microsoft.com/ja-jp/azure/app-service/deploy-continuous-deployment?tabs=github#prepare-your-repository)』
- Docs: 『[Custom Deployment Script](https://github.com/projectkudu/kudu/wiki/Custom-Deployment-Script)』
- Docs: 『[Customizing deployments](https://github.com/projectkudu/kudu/wiki/Customizing-deployments)』

ここからは、デプロイ用スクリプトのカスタマイズを、Java アプリケーションへ適用する方法を試してみます。

**なお、この方法は Docs に公開されておらず、公式にサポートされていない可能性があります。商用環境への適用は、ご自身の責任で行っていただけますようお願いいたします。**

## デプロイ構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/8ea20799-c038-e381-43c0-ed6995df3858.png)

App Service によって自動生成されるデプロイ用スクリプトは、下記の２つがあります。

| ファイル名 | 役割 |
| :--- | :--- |
| `.deployment` | 環境変数の設定・実行するスクリプトの指定 |
| `deploy.cmd` または `deploy.sh` | 実行されるスクリプト |

デプロイ用スクリプトの実体は、`deploy.cmd` または `deploy.sh` です。

Docs: 『[Custom Deployment Script](https://github.com/projectkudu/kudu/wiki/Custom-Deployment-Script)』

### 自動生成されるデプロイ用スクリプトの実体解剖

デプロイ用スクリプトの実体が `deploy.cmd` または `deploy.sh` ということがわかったので、その中身を紐解いていきます。

これらスクリプトの中身は、大きく３つのステップで構成されています。

1. 前処理
1. KuduSync
1. 後処理

このうち、`1. 前処理` または `3. 後処理` フェーズのいずれかで、アプリケーションのビルドに相当するコマンドが実行されます。ASP.NET Core の場合は MSBuild、Node.js の場合は npm などですね。

しかし、自動生成されるデプロイ用スクリプトに必ず前処理、後処理が含まれるわけではありません。前処理、後処理が含まれるかどうかは、以下の条件によって決められます。

| ランタイム | 条件（ルートフォルダに次のファイルのいずれかが存在すること） |
| :--- | :--- |
| ASP.NET | *.sln、 *.csproj、または default.aspx |
| ASP.NET Core | *.sln または *.cspro |
| PHP | Index.php |
| Ruby | Gemfile |
| Node.js | Server.js, app.js または スタートスクリプトを含む package.json |
| Python | *.py, requirements.txt または runtime.txt |
| HTML | default.htm、default.html、default.asp、index.htm、index.html、または iisstart.htm |

自動生成されるデプロイ用スクリプトに必ず含まれているのは、KuduSync だけです。もし、上記の前処理・後処理の条件に合致しなかった場合、KuduSync のみが実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/4c2039a4-705d-4b24-64da-e3ca0d5ab7ec.png)

KuduSync は ドキュメントルート(既定では `site/wwwroot` )へ ビルド結果などのコンテンツのコピーを行いますが、コピーの前に前回実行時にコピーしたコンテンツを削除します。KuduSync 処理以外でコピーされたファイルは削除されません。

### デプロイ用スクリプトのカスタマイズ（Java 用）

Windows を App Service の OS として使用している際、一度でもデプロイしている場合は、自動生成されたデプロイ用スクリプトを Kudu UI からダウンロードすることが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/ebe37546-c8e4-e6ac-05d9-d1ea81e54d17.png)


### サーバーサイドビルドを実行

デプロイ用スクリプトをカスタマイズし、サーバーサイドビルドを実行する際は、`.deployment` と `deploy.cmd` の2つファイルがプロジェクトルートに存在する必要があります。

```.deployment
[config]
command = ./deploy.cmd
```

```deploy.cmd
@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off

:: ----------------------
:: KUDU Deployment Script
:: Version: 1.0.17
:: ----------------------

:: Prerequisites
:: -------------

:: Verify node.js installed
where node 2>nul >nul
IF %ERRORLEVEL% NEQ 0 (
  echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment.
  goto error
)

:: Setup
:: -----

setlocal enabledelayedexpansion

SET ARTIFACTS=%~dp0%..\artifacts
SET JAVA_CUSTOM_DEPLOYMENT=1
SET DEPLOYMENT_SOURCE=%~dp0%
SET DEPLOYMENT_TARGET=%~dp0%target

IF NOT DEFINED DEPLOYMENT_SOURCE (
  SET DEPLOYMENT_SOURCE=%~dp0%.
)

IF NOT DEFINED DEPLOYMENT_TARGET (
  SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot
)

IF NOT DEFINED NEXT_MANIFEST_PATH (
  SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest

  IF NOT DEFINED PREVIOUS_MANIFEST_PATH (
    SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest
  )
)

IF NOT DEFINED KUDU_SYNC_CMD (
  :: Install kudu sync
  echo Installing Kudu Sync
  call npm install kudusync -g --silent
  IF !ERRORLEVEL! NEQ 0 goto error

  :: Locally just running "kuduSync" would also work
  SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd
)

IF NOT DEFINED JAVA_DEPLOYMENT_CMD (
  SET JAVA_DEPLOYMENT_CMD=%~dp0%java_deployment.cmd
)

goto Deployment

:: Utility Functions
:: -----------------

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

:Deployment
echo Handling java deployment...

:: 1. Install maven dependencies
IF /I "%JAVA_CUSTOM_DEPLOYMENT%" EQU "1" (
  echo deploying java app...
  call :ExecuteCmd "%JAVA_DEPLOYMENT_CMD%"
)

:: 2. KuduSync

call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 !IGNORE_MANIFEST_PARAM! -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
IF !ERRORLEVEL! NEQ 0 goto error

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto end

:: Execute command routine that will echo out when error
:ExecuteCmd
setlocal
set _CMD_=%*
call %_CMD_%
if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_%
exit /b %ERRORLEVEL%

:error
endlocal
echo An error has occurred during web site deployment.
call :exitSetErrorLevel
call :exitFromFunction 2>nul

:exitSetErrorLevel
exit /b 1

:exitFromFunction
()

:end
endlocal
schtasks /create /tn "Deploy zip to kudu" /tr %~dp0%curl.cmd /sc once /delay 5:00
echo Finished successfully.
```

最終的なプロジェクト構造は以下のようになります。

```
$ tree .
.
|-- deploy.cmd
|-- deploy.sh
|-- java_deployment.cmd
|-- mvnw
|-- mvnw.cmd
|-- pom.xml
|-- settings.xml
|-- src
|   |-- main
・・・
```

実際に、サーバーサイドビルドを実行します。

```
% git push -u azure develop                                                            
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 10 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 663 bytes | 663.00 KiB/s, done.
Total 9 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Deploy Async
remote: Updating branch 'develop'.
remote: Updating submodules.
remote: Preparing deployment for commit id '8028482d5d'.
remote: PreDeployment: context.CleanOutputPath False
remote: PreDeployment: context.OutputPath /home/site/wwwroot
remote: Running custom deployment command...
remote: Setting execute permissions for /home/site/repository/deploy.sh
remote: Running deployment command...
remote: Handling Java app deployment.
remote: deploying java app...
remote: ++++ find target/ -name '*.jar' -not -name '*-plain.jar'
remote: +++ APP_JAR=target/customdeployment-0.0.1-SNAPSHOT.jar
remote: +++ curl -O -k https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
remote:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
remote:                                  Dload  Upload   Total   Spent    Left  Speed
remote: 
remote:   0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
remote:   0 9283k    0 16384    0     0  21932      0  0:07:13 --:--:--  0:07:13 21933
remote:  68 9283k   68 6368k    0     0  3913k      0  0:00:02  0:00:01  0:00:01 3913k
remote: 100 9283k  100 9283k    0     0  5312k      0  0:00:01  0:00:01 --:--:-- 5310k
remote: +++ tar -xzf apache-maven-3.6.3-bin.tar.gz
remote: +++ cp ./settings.xml apache-maven-3.6.3/conf/settings.xml
remote: +++ cat apache-maven-3.6.3/conf/settings.xml
remote: <?xml version="1.0" encoding="UTF-8"?>
remote: +++ pwd
remote: <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
remote: +++ apache-maven-3.6.3/bin/mvn clean package
remote:           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
remote: The JAVA_HOME environment variable is not defined correctly
remote:           xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
remote: This environment variable is needed to run this program
remote:   <!-- <localRepository>C:/home/.m2</localRepository> -->
remote: NB: JAVA_HOME should point to a JDK not a JRE
remote:   <localRepository>/home/.m2</localRepository>
remote: +++ cp /home/site/repository/target/customdeployment-0.0.1-SNAPSHOT.jar /home/site/wwwroot/app.jar
remote:   <servers></servers>
remote:   <mirrors></mirrors>
remote:   <profiles></profiles>
remote: </settings>
remote: /home/site/repository
remote: Running post deployment command(s)...
remote: Triggering recycle (preview mode disabled).
remote: Deployment successful.
remote: Deployment Logs : 'https://app-javaonazure-customdeploy-linux.scm.azurewebsites.net/newui/jsonviewer?view_url=/api/deployments/8028482d5dc345bd587edc23889160c054b00ecc/log'
To https://app-javaonazure-customdeploy-linux.scm.azurewebsites.net:443/app-javaonazure-customdeploy-linux.git
   6242bca..8028482  develop -> devekio
branch 'master' set up to track 'origin/master'.
```

このように、サーバーサイドビルドに成功しました。

## Tips

ここからは、実際に動かす際に詰まったポイントを挙げていきます。

### デプロイ結果を反映するには、アプリケーションの再起動が必須

デプロイ後、稼働中の Java アプリケーションにデプロイ時の変更点を反映させる場合、App Service を再起動する必要があります。

### （Windows の場合）Maven のローカルレポジトリを App Service の共有ディレクトリに配置する必要がある

既定では、`${user.home}/.m2/repository` に Maven のローカルレポジトリが配置され、依存関係にある jar ファイルがダウンロードされます。

しかし、Maven 既定の設定値だと、以下のようにディスク領域が不足してしまい、リモートビルドに失敗します。

```
remote: java.io.IOException: There is not enough space on the disk
remote:     at java.io.RandomAccessFile.writeBytes (Native Method)
remote:     at java.io.RandomAccessFile.write (RandomAccessFile.java:546)
・・・
```

よって、Maven のローカルレポジトリは、App Service の共有フォルダを指定する必要があります。