pnpm で作った動的な Next.js を Azure App Service に Deploy する

## やりたいこと
`pnpm` で `create next-app` した Next.js を Azure App Service に Deploy したい。Deploy にアホほど時間がかかるのもなんとかしたい。

しばらく前にやったのと、結構内容が変わったような気がするので、改めて。

前にやったヤツ
> [動的な Next.js を Azure App Service に Deploy する](https://qiita.com/kurab/items/46352a65ee946eee9c76)

以前は、VS Code から Deploy したが、チーム開発の場合不便なので、今回は GitHub Actions で Deploy する。


## 環境

- Node.js: 20.17.0
- pnpm: 9.9.0

## 最初の Setup

### Next.js アプリケーション

```bash
pnpm create next-app@canary deploy-test
```

今回は、訳あって version 15 の Next.js を使う。

- next.js: 15.0.0-canary.140
- react: 19.0.0-rc-7771d3a7-20240827
- react-dom: 19.0.0-rc-7771d3a7-20240827

がインストールされた。Next.js v14 等でも以下の話は同じである。
canary やら rc やら beta やらをサーバに Deploy したら、何か不具合あるかな？と思ったが、当然のように何の問題もなかった。

`pnpm dev`, `pnpm build`, `pnpm start` などを一通り試し、問題なく動くことを確認。

GitHub にリポジトリを作成し、push。

### Azure App Service
Azure Portal から GitHub と連携するように設定して App Service を立ち上げる。
Node のバージョンは、20 で。

そうすると、GitHub リポジトリに以下のファイルが追加され、GitHub Actions が走る。

```yml:.github/workflows/main_deploytest.yml
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy Node.js app to Azure Web App - deploytest

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js version
        uses: actions/setup-node@v3
        with:
          node-version: '20.x'

      - name: npm install, build, and test
        run: |
          npm install
          npm run build --if-present
          npm run test --if-present

      - name: Zip artifact for deployment
        run: zip release.zip ./* -r

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v4
        with:
          name: node-app
          path: release.zip

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'Production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
    permissions:
      id-token: write #This is required for requesting the JWT

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v4
        with:
          name: node-app

      - name: Unzip artifact for deployment
        run: unzip release.zip
      
      - name: Login to Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_xxxx }}
          tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_yyyy }}
          subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_zzzz }}

      - name: 'Deploy to Azure Web App'
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v3
        with:
          app-name: 'deploytest'
          slot-name: 'Production'
          package: .
```

Dependencies のインストールや build などは `npm` で行われるが、build 等でエラーは出ないので、いったんそのまま完了を待つ。

14分くらいで Deploy が完了したが…

![Screenshot 2024-09-04 at 10.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245908/e9168b43-fc38-c316-3c7b-286c07c75c81.png)

残念。:(

ログを見ながら、問題点を潰していくところだが、npm を pnpm に変えたり、ポートを変えたり、先にやっておきたいことがあるので、まずはそれをやる。

## 基本設定

### Node のバージョンを固定する

あんまり気にしなくても良さそうだが、Node.js は 20.17.0 に固定する。

#### Local
いらないかも知れないが、一応。

```bash
$ volta pin node@20.17.0
```

package.json に以下が追加される。

```json:package.json
  "volta": {
    "node": "20.17.0"
  }
```

弊社は、volta 利用。

```json:package.json
  "engines": {
    "node": "20.17.0"
  }
```
これも追加しておく。

#### GitHub Actions

Build 時に利用される Node のバージョンを指定する。

```yml:.github/workflows/main_deploytest.yml
      - name: Set up Node.js version
        uses: actions/setup-node@v4
        with:
          node-version: '20.17.0'
```
`20.x` を `20.17.0` に変更。`actions/setup-node` も `v3` から `v4` に変更。v3と4の違いは、キャッシュ機能が強化されているようで、デフォルトで v3 が指定されるが、 v4 を使う方が良い。

### npm ではなく pnpm を使うようにする

```yml:.github/workflows/main_deploytest.yml
      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9.9.0
          run_install: false

      - name: Set up Node.js version
        uses: actions/setup-node@v4
        with:
          node-version: "20.17.0"
          cache: "pnpm"

      - name: Install Dependencies
        run: pnpm install

      - name: Build Application
        run: pnpm run build
```

`App Service > 設定 > 構成 > スタックの設定 > スタートアップコマンド` は、 `pnpm run start` ではうまく動かないそうなので（ちゃんと調べてない）、 `npm run start` としておく。

### PORT を変更する

Azure App Service は、デフォルトで 8080 ポートを Listen する。環境変数で、3000 を指定しても良いが、3000 じゃなきゃダメな理由もないので、Production は、8080 で立ち上がるように package.json に手を加える。

```json:package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p 8080",
    "lint": "next lint"
  },
```

一旦、ログを見るまでもなくやっとく設定は完了。

これで一旦 Deploy してみる。が、めちゃくちゃ時間がかかるようになってしまった。1時間くらいかかった。サイズが 86MB から 466MB に増えている。




## エラーログを確認

```
Error: Could not find a production build in the '.next' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id
```

`.next` ディレクトリがないと。そりゃ動きません。本当にないのか？を確認するため、build job 中に Zip に圧縮したファイルをダウンロードして見てみると…確かにないです。つまり build したものが、App Service のコンテナに届いていないということですね。

原因は、

```yml:.github/workflows/main_deploytest.yml
      - name: Zip artifact for deployment
        run: zip release.zip ./* -r
```

ここですね。`.next` 等の隠しディレクトリを含めたい場合は、

`zip release.zip ./* -r` ではなく、`zip release.zip . -r` とする必要がある。
のですが、リリースのたびに1時間も待ってられないので、それも含めて対応します。

### 対応方針

1. まず、サイズが大きくなってしまっているので、`output: 'standalone'` にする
1. build したファイル郡（.next/standalone）だけを Zip 圧縮してやる
1. スタートアップコマンドは、`node server.js` に変更する（.next/standalone 配下のみがリリースされるので）
1. PORT = 8080 を環境変数に設定する

#### standalone

```ts:next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

```

```json:package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
    "start": "next start",
    "lint": "next lint"
  }
```

`.next/standalone` 配下に必要なものが全て揃うように、コピーをするようにした。start の -p 8080 は、start コマンドで起動しなくなったので、削除。

#### zip

```yml:.github/workflows/main_deploytest.yml
      - name: Zip artifact for deployment
        run: |
          cd .next/standalone
          zip ../../release.zip . -qr
          cd ../../
```

スタートアップコマンドと、PORT は Azure Portal から設定。

準備完了。

## 身に覚えのないモジュールでエラー

```bash
Error: Cannot find module 'styled-jsx/package.json'
```

Next.js が内部的に使っているらしい。GitHub Copilot に聞くと、styled-jsx をインストールすれば解決すると言われたが、解決しない。そして1つ解決しても次、次、と際限ないヤツっぽい。

これは、pnpm の node_modules の取り扱いと、Azure の symlink の取り扱いの不整合による。どちらかと言えば、Azure 側に原因があるようだ。

pnpm はデフォルトで、`node_modules/.pnpm` 配下にモジュールをインストールし、そこに対して symlink を作成する。

Azure は symlink をデプロイすると、symlink のままではなく、symlink の参照先を実体化する。その際に相対パスが壊れる。

どうやらこういうことらしい。

これに対するうまい解決策が思いつかなかったので、symlink でインストールしないように変更した。

> [pnpm Settings - node-linker](https://pnpm.io/npmrc#node-linker)

ルートディレクトリに、.npmrc を設置。

```.npmrc
node-linker=hoisted
```

こうすると、`node_modules` 配下にモジュールをインストールし、symlink を作らないようになる。この解決策が本当に良いのかどうかは、よく分からないが、Azure が「symlink を実体化してしまう」のであれば、容量的にはコンパクトになっている…かな。いったん良しとした。


## 結果

![Screenshot 2024-09-04 at 15.54.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245908/6393723c-dbed-03f1-13fa-2116d030d48e.png)

できました！Deploy にかかった時間も、前回（最後に Deploy には成功した時）が 64 分でしたが、今回は 5 分ジャストで完了。92%短縮！

人は、たたきがないと動き出す腰が重いものだが、自分が書いてないそれっぽいものの中から間違いを見つけるのは非常に大変なものである。

## おまけ: 環境変数

なお、Build 時に必要な環境変数は、該当リポジトリの

`settings > Secrets and variables > Actions > Repository secrets` で設定し、
GitHub Actions のファイル内にて、

```yml
      - name: Build
        run: pnpm run build
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
```

こんな感じで呼び出す。