しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

Node.js アプリを Azure App Service へ最適な形でデプロイする

何となく 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 cinpm 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 にビルドした結果のファイルが保存されているはずです。

f:id:shiba-yan:20190807014810p:plain

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 が追加されていることが確認できるはずです。

f:id:shiba-yan:20190807014822p:plain

実行に必要なファイルは全てこの 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 へデプロイを行います。

デプロイタスクは何回も書いているので省略しますが、動かすと以下のような実行履歴になります。

f:id:shiba-yan:20190807015400p:plain

Run From Package を使っているので、デプロイが短時間で済んでいます。

これが通常の ZipDeploy の場合は node_modules の展開に時間がかかってしまいますが、Run From Package では zip のまま扱うので非常に高速です。

デプロイ後に App Service をブラウザで開いてみると、アプリが動いていることが確認できるはずです。

f:id:shiba-yan:20190807015433p:plain

Hello World をデプロイしただけなので味気ないですが、Nest.js で作成したアプリが動作しています。

App Service のストレージが遅い点を Run From Package で回避しているので、所謂コールドスタート問題を割と改善出来るはずですが、サンプルアプリ程度では大きな差は出ませんでした。