何となく package.json
を眺めていて、これまでは devDependencies
も含めた形でデプロイしていたことがあったと思ったので、最適なパッケージをビルドしてデプロイする方法を確認しました。
実際は Docker Image を Multi-stage build で作成するのと考え方は同じだと思います。以下の 2 点を理解しておけば問題なさそうです。
- 動作に必要なものだけデプロイする
- 開発時のみ必要なパッケージを
node_modules
に含めない
- 開発時のみ必要なパッケージを
- Run From Package / Docker Image を使う
Linux の App Service 向けではビルド済みの Docker Image を使っておけば間違いないです。
テスト用の Hello World アプリは流行ってそうな雰囲気があった Nest.js を使ってみました。軽くドキュメントを読んだ感じでは ASP.NET Core MVC に近い書き方な気がします。
適当に CLI をインストールして、そのままサンプルアプリを用意しました。今回は Nest.js を使っていますが、基本的には Node.js で動くアプリなら同じなはずです。
デプロイ用のパッケージ作成は、これまで通り Azure Pipelines を使いました。以下、定義となります。
アプリケーションをビルド
ビルドは深く考えずに npm ci
と npm run build
を使ってアプリをビルドし、その結果を Pipeline Artifacts に保存しているだけの簡単な定義です。
stages: - stage: Build jobs: - job: Build pool: vmImage: 'ubuntu-latest' steps: - task: NodeTool@0 inputs: versionSpec: '10.x' displayName: 'Install Node.js' - script: | npm ci npm run build displayName: 'npm ci and build' - publish: dist artifact: dist
ビルドなので devDependencies
を含めたパッケージをインストールしています。
実行すると Artifacts にビルドした結果のファイルが保存されているはずです。
Pipeline Caching を使っても良いですが、今回の本質からずれるので省略しています。
デプロイ用にパッケージング
肝心なのがデプロイ用のパッケージを作成する処理です。Multi-stage build で言うところの最後のイメージ生成に当たる部分です。
この場合は実行に必要なパッケージだけ含めれば良いので、package*.json
を別ディレクトリにコピーしてから、npm ci --production
を実行するようにしています。
- stage: Packaging dependsOn: Build jobs: - job: Publish pool: vmImage: 'ubuntu-latest' steps: - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' targetPath: '$(Build.SourcesDirectory)' - task: CopyFiles@2 inputs: Contents: | dist/**/* package*.json web.config TargetFolder: 'publish' - task: NodeTool@0 inputs: versionSpec: '10.x' displayName: 'Install Node.js' - script: | npm ci --production workingDirectory: 'publish' displayName: 'npm ci --production' - task: ArchiveFiles@2 inputs: rootFolderOrFile: 'publish' includeRootFolder: false archiveType: 'zip' archiveFile: '$(Build.BuildNumber).zip' - publish: $(Build.BuildNumber).zip artifact: packed
Run From Package でデプロイするためには zip が必要なので、このタイミングで zip にしておきました。
Artifacts を見ると、新しく zip が追加されていることが確認できるはずです。
実行に必要なファイルは全てこの zip に含まれているので、後は zip を Azure Web App のタスクを使ってデプロイすれば完了です。
補足 : iisnode の代わりに HttpPlatformHandler を使う
App Service (Windows) では Node.js アプリの実行には iisnode が使われるようになっていますが、一般的な 実行方法とは異なるので挙動が違ったり、設定が煩雑だったりします。
今回は HttpPlatformHandler を使って、Node.js アプリを実行するようにしました。Web.config は以下のようにシンプルですが、arguments
の値はアプリ毎に変える必要があります。
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="httpPlatformHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified" /> </handlers> <httpPlatform processPath="node" arguments=".\dist\main.js"> <environmentVariables> <environmentVariable name="PORT" value="%HTTP_PLATFORM_PORT%" /> <environmentVariable name="NODE_ENV" value="production" /> </environmentVariables> </httpPlatform> </system.webServer> </configuration>
Nest.js の場合は npm run start:prod
を使うので、実際のコマンドを拾ってきて設定しています。ここでは npm は使えないようになっているので、注意したいところです。
使用するポート番号は環境変数から取る必要があるので main.ts
を少しだけ弄ります。
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT || 3000); } bootstrap();
process.env.PORT
は割とよく見るコードですね。これで全ての準備が整いました。
デプロイと動作確認
Azure Pipelines を使って作成しておいた App Service へデプロイを行います。
デプロイタスクは何回も書いているので省略しますが、動かすと以下のような実行履歴になります。
Run From Package を使っているので、デプロイが短時間で済んでいます。
これが通常の ZipDeploy の場合は node_modules
の展開に時間がかかってしまいますが、Run From Package では zip のまま扱うので非常に高速です。
デプロイ後に App Service をブラウザで開いてみると、アプリが動いていることが確認できるはずです。
Hello World をデプロイしただけなので味気ないですが、Nest.js で作成したアプリが動作しています。
App Service のストレージが遅い点を Run From Package で回避しているので、所謂コールドスタート問題を割と改善出来るはずですが、サンプルアプリ程度では大きな差は出ませんでした。