App Service on Linux Node runtime における ローカルビルドのデプロイ時間を短縮する工夫

# はじめに

この記事は、以下の記事の続編です。

https://qiita.com/georgeOsdDev@github/items/a4aa7b7aa43a0114d96f

https://qiita.com/georgeOsdDev@github/items/7884fabcabc1cb30eb88

各デプロイ方法を用いた場合に、少しでも時間を短縮するTipsを記載していきます。


# ローカルビルドを利用する場合のTips

ローカルビルドを利用する際に主に時間がかかる箇所は、`/tmp` に展開した zip の中身を `/home/site/wwwroot` にコピーする箇所となっている。

- `Kudu sync from: '/tmp/zipdeploy/extracted' to: '/home/site/wwwroot'`


なお、Kudu sync は、直前のデプロイ内容との差分を見ているため、2回目以降はある程度早くなります。

https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url#comparison-with-zip-api

> **Deletion of files no longer in the build**: When a new build is deployed with zipdeploy, files and directories that were created by the previous deployment but are no longer present in the build will be deleted. Any other files and directories found in the site that aren't being overwritten by the deployment, such as those placed there via FTP or created by your app during runtime, will be preserved.
**Efficient file copy**: Files will only be copied if their timestamps don't match what is already deployed. Generating a zip using a build process that caches outputs can result in faster deployments.

## 作戦

リモートビルドの動作で確認したように、アプリケーション起動時に `node_modules.tar.gz` を展開して利用する仕組みがあります。
これを利用することで、ローカルビルドした後に、予め `node_modules` を圧縮した状態で zip デプロイすることで、`node_modules` 配下のコピーを抑えることができそうです。

### `node_modules.tar.gz` が利用される条件

アプリケーション起動時に`node_modules.tar.gz` が利用されるようにするには、アプリケーションコンテナ内の `/opt/starup/startup.sh` がそのように構成される必要があります。
これまでの記事で見てきたように、`/opt/starup/startup.sh` は、`oryx create-script -appPath /home/site/wwwroot -output $STARTUP_COMMAND_PATH -defaultApp=/opt/startup/default-static-site.js -userStartupCommand '$@'` コマンドによって生成されます。

crate-script 時には `oryx-manifest.toml` をもとに、起動ファイルを生成します。

https://github.com/microsoft/Oryx/blob/main/doc/configuration.md#oryx-generated-manifest-file

> oryx-manifest.toml is the orxy-generated manifest file involved in 2 stages (build & run). Full flow explanation by platforms. First the build stage will build the application and autogenerate oryx-manifest.toml. Secondly oryx-manifest.toml is used in the runtime to determine how to run the application. The following are fields supported today inside oryx-manifest.toml:

上記のドキュメントに記載はありませんが、ソースを見る限り、`CompressedNodeModulesFile` というフィールドに定義されている場合に、展開処理が追記されるように見えます。

https://github.com/microsoft/Oryx/blob/main/src/startupscriptgenerator/src/node/scriptgenerator.go#L91-L134

本来、Oryx は build 時に生成した、`oryx-manifest.toml` を生成しますが、ローカルビルド利用時には、`oryx-manifest.toml` は存在しません。そのため、リリース時に利用する zip に含めて置く必要がありそうです。

### 検証

`package.json` に `postbuild` を追加し、ローカル環境でのビルド実行後に、`/node_modules.tar.gz` を作成するようにしておきます。

```package.json
"postbuild": "sh preapareLocalCompressedNodeModule.sh"
```

```bash:preapareLocalCompressedNodeModule.sh
#!/usr/bin/env bash
echo "`date "+%Y-%m-%d %H:%M:%S"` preapareLocalCompressedNodeModule started"
if [ -f "./node_modules.tar.gz" ]; then
  rm -rf ./node_modules.tar.gz
fi
cd node_modules
tar -zcf ../node_modules.tar.gz .
cd ../
rm -rf ./node_modules
rm -rf oryx-manifest.toml
echo 'PlatformName="nodejs"' >> oryx-manifest.toml
echo 'CompressedNodeModulesFile="node_modules.tar.gz"' >> oryx-manifest.toml
echo "`date "+%Y-%m-%d %H:%M:%S"` preapareLocalCompressedNodeModule end"
```

VS Codeからデプロイ時にローカルビルドを用いるように変更します。

```json:.vscode/settings.json
{
  "appService.showBuildDuringDeployPrompt": false,
  "appService.preDeployTask": "npm: build"
}
```

また、リモートビルドを実行するように構成された `.deployment` ファイルは削除しておきます。

### 結果

例によって、予め `/home/site/wwwroot` は削除した状態で試してみます。

ローカルマシン上で`postbuild` で行った処理は14秒でした。

```
> next-app-sample-ts@0.1.0 postbuild
> sh preapareLocalCompressedNodeModule.sh

2023-03-16 19:21:47 preapareLocalCompressedNodeModule started
2023-03-16 19:22:01 preapareLocalCompressedNodeModule end
```

つづいて、VS Code のデプロイ処理
7:22:02 Starting deployment...　から 7:22:24 Triggering recycle まで 22秒でした。

[工夫しない場合](https://qiita.com/georgeOsdDev@github/items/a4aa7b7aa43a0114d96f#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%83%93%E3%83%AB%E3%83%89--zip-%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%AE%E5%AE%9F%E8%A1%8C) は、初回デプロイで 6分3秒かかっていたのに比べると、13倍速です。

```Output
7:22:02 PM node-deploy-speed-test: Starting deployment...
7:22:03 PM node-deploy-speed-test: Creating zip package...
7:22:08 PM node-deploy-speed-test: Zip package size: 99.7 MB
7:22:09 PM node-deploy-speed-test: Fetching changes.
7:22:10 PM node-deploy-speed-test: Cleaning up temp folders from previous zip deployments and extracting pushed zip file /tmp/zipdeploy/f4f5e638-100b-4222-a492-e3d3479429d5.zip (95.04 MB) to /tmp/zipdeploy/extracted
7:22:15 PM node-deploy-speed-test: Updating submodules.
7:22:16 PM node-deploy-speed-test: Preparing deployment for commit id 'e3fe5d7f-7'.
7:22:16 PM node-deploy-speed-test: PreDeployment: context.CleanOutputPath False
7:22:17 PM node-deploy-speed-test: PreDeployment: context.OutputPath /home/site/wwwroot
7:22:17 PM node-deploy-speed-test: Generating deployment script.
7:22:17 PM node-deploy-speed-test: Using cached version of deployment script (command: 'azure -y --no-dot-deployment -r "/tmp/zipdeploy/extracted" -o "/home/site/deployments/tools" --basic --sitePath "/tmp/zipdeploy/extracted"').
7:22:17 PM node-deploy-speed-test: Running deployment command...
7:22:17 PM node-deploy-speed-test: Command: "/home/site/deployments/tools/deploy.sh"
7:22:18 PM node-deploy-speed-test: Handling Basic Web Site deployment.
7:22:18 PM node-deploy-speed-test: Kudu sync from: '/tmp/zipdeploy/extracted' to: '/home/site/wwwroot'
7:22:18 PM node-deploy-speed-test: Copying file: '.eslintrc.json'
7:22:18 PM node-deploy-speed-test: Copying file: '.gitignore'
7:22:18 PM node-deploy-speed-test: Copying file: 'README.md'
7:22:18 PM node-deploy-speed-test: Copying file: 'azureappservice_custombuild.sh'
7:22:18 PM node-deploy-speed-test: Copying file: 'azureappservice_postbuild.sh'
7:22:19 PM node-deploy-speed-test: Copying file: 'azureappservice_prebuild.sh'
7:22:19 PM node-deploy-speed-test: Copying file: 'build_flow.md'
7:22:19 PM node-deploy-speed-test: Copying file: 'next-env.d.ts'
7:22:19 PM node-deploy-speed-test: Copying file: 'next.config.js'
7:22:19 PM node-deploy-speed-test: Copying file: 'node_modules.tar.gz'
7:22:21 PM node-deploy-speed-test: Copying file: 'oryx-manifest.toml'
7:22:21 PM node-deploy-speed-test: Copying file: 'package-lock.json'
7:22:21 PM node-deploy-speed-test: Copying file: 'package.json'
7:22:22 PM node-deploy-speed-test: Copying file: 'preapareLocalCompressedNodeModule.sh'
7:22:22 PM node-deploy-speed-test: Copying file: 'tsconfig.json'
7:22:22 PM node-deploy-speed-test: Ignoring: .git
7:22:22 PM node-deploy-speed-test: Copying file: '.next/BUILD_ID'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/build-manifest.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/export-marker.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/images-manifest.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/next-server.js.nft.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/package.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/prerender-manifest.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/react-loadable-manifest.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/required-server-files.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/routes-manifest.json'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/trace'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/cache/.tsbuildinfo'
7:22:22 PM node-deploy-speed-test: Copying file: '.next/cache/next-server.js.nft.json'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/eslint/.cache_s5keyn'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/0.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/1.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/2.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/3.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/4.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/5.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/6.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/7.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/8.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/9.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/index.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/client-production/index.pack.old'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/server-production/0.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/server-production/1.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/server-production/index.pack'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/cache/webpack/server-production/index.pack.old'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/server/font-manifest.json'
7:22:23 PM node-deploy-speed-test: Copying file: '.next/server/middleware-build-manifest.js'
7:22:24 PM node-deploy-speed-test: Copying file: '.next/server/middleware-manifest.json'
7:22:24 PM node-deploy-speed-test: Omitting next output lines...
7:22:24 PM node-deploy-speed-test: Finished successfully.
7:22:24 PM node-deploy-speed-test: Running post deployment command(s)...
7:22:24 PM node-deploy-speed-test: Triggering recycle (preview mode disabled).
7:22:24 PM node-deploy-speed-test: Deployment successful. deployer = ms-azuretools-vscode deploymentPath = ZipDeploy. Extract zip.
7:22:40 PM: Deployment to "node-deploy-speed-test" completed.
```

`$HOME/LogFiles/YYYY_MM_DD_<MACHINENAME>_default_docker.log` で実行時の展開には18秒でした。

```$HOME/LogFiles/YYYY_MM_DD_<MACHINENAME>_default_docker.log
2023-03-16T10:22:39.507560292Z Found tar.gz based node_modules.
2023-03-16T10:22:39.509133101Z Removing existing modules directory from root...
2023-03-16T10:22:39.569990455Z Extracting modules...
2023-03-16T10:22:57.569748327Z Done.
```

ローカルでの圧縮(+14)+VSCodeによるデプロイ(+22)+アプリ実行時の展開(+18) 合計で1分弱となりました。

なお、node_modules および、アプリコードを変更してデプロイしてもほぼ時間は変わらずでした。

大成功っぽいですが、`oryx-manifest.toml` を自前で用意することの是非については要注意と思います。

次回は、同じ要領で GitHub Actions を利用した場合に高速化手法を考えてみます。