valid,invalid

関心を持てる事柄について

Now.sh v1 to v2 & Next.js v7 to v9 アップグレード記

Goofiの利用しているNow.shをv1からv2に移行し、その過程でNext.js versionも7.0.2から9.1.1へupgradeした。 Now.shもNext.jsもしばらく全く追いかけていなかったのでやや苦労した。

変更内容は https://github.com/ohbarye/goofi/pull/172

背景: Now v2

もはや一年以上前のことだが、Now.shが2.0になったという発表があった。

2018年の夏頃に自分が開発していたGoofiはNow v1を利用していたので「差分はよう知らないけどそのうち移行しなきゃ」ぐらいに捉えていた…が、結果1年ぐらい放置してしまっていた。

最初はnow.json*1をちょっと書き換えたら動くやろと思って、特に何のリファレンスも見ずにNow.shをupgradeしてみた。localでPaaSの挙動は試せないと思っててきとうにproductionでもエイヤっと上げてしまったがそこからが大変だった。

Next.js upgrade

まず、serverプロセスが立ち上がらない。Now.shのdashboard on webを見るとクラッシュしている。調べてみたところ当時のGoofiの構成(以下)はNow v2ではもはや動かないとのこと。

  • Next.jsとExpress.jsを組み合わせ、serverとしてのプロセスをNow platform上で立ち上げる
  • initial requestでSSRしてresponseを返す
  • 以降はAPI serverとして振る舞う

v2声明文にあるようにNow v2はserverlessを志向しており、利用者のコードを静的ファイルに変換する、またはリクエスト毎に実行可能なserverless functionに変換し、そのplatform上にdeployする。

Behind the scenes, Now 2.0 works like an extensible build system and compiler, capable of transforming your sources into static files and serverless functions (lambdas) for production.

つまりserverプロセス立ち上げっぱなしというのがそもそもNow v2の思想からは外れていそうだということ。

幸い、というか必然*2にしてNext.jsもv8からserverless対応しているので、platformの変化に合わせてupgradeしてやれば良さそうだった。

pages/ directory以下にページごとの実装を書いていくのはNext.js v7までと同じだが、v8からはserverless modeというのが追加されており、pages/以下のファイルがserverless functionに変換可能になる。

The serverless target will output a single lambda per page. This file is completely standalone and does not require any dependencies to run:

また、v9からはAPI Routesという機能が追加されており、pages/api/ directory以下に以下のようなfunctionを書くだけでserverless functionになる。面白い。

import { NextApiRequest, NextApiResponse } from "next";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { id } = req.query;
  const result = await doAsyncSomething({ id });
  res.setHeader("Content-Type", "application/json; charset=utf-8");
  res.send(result);
};

f:id:ohbarye:20191208005021p:plain
Next.jsのレールに乗ってserverless functionなどがbuildされるようす

Next.jsはこのようにしてv9までupgradeしていった。

その他、細かい課題と変更

大きくはNext.jsのupgrade対応に時間を費やしたのだが、その他にも諸々の変更が必要だったので思い出せるものをメモしておく。

TypeScript対応

Next.js v9からはTypeScriptがデフォルトでサポートされ、@zeit/next-typescriptが不要になっていた。このあたりも多少のコード修正は必要だったがTypeScript化の恩恵を感じて体験が良かった。

publicRuntimeConfigが利用不可

next.config.jsに書いていたpublicRuntimeConfigがNow v2では利用不可になっていた。

https://github.com/zeit/next.js/blob/master/errors/serverless-publicRuntimeConfig.md

実行時ではなくbuild時に値を埋め込むことは可能なのでその方向で修正し、値にはprocess.env.*でアクセスするようにした。

https://nextjs.org/docs#build-time-configuration

Expressやめる

上述の通り、serverless modeではExpressサーバプロセスを起動しっぱなしにすることはできない。 Expressサーバに実装していたAPI endpointsは pages/api/ に処理を移した。

npm script

Expressを使っていた時はyarn devでlocal開発を開始していたが、next commandを使うことになった。

また、productionでサーバプロセスを起動することはないので用意していたyarn startは不要になった。

// package.json
  "scripts": {
-    "dev": "node ./server/index.js",
+    "dev": "next",
    "build": "next build",
-    "start": "NODE_ENV=production node ./server/index.js",
    "type-check": "tsc"
  },

apollo-server-express to apollo-server-micro

これもExpressやめたことに伴う対応。 apollo-server-expressではなくapollo-server-microを利用するようにした。

microもZEITによるOSS。よくわかってないけどGitHubのREADME読む感じ、ZEITの志向性がバリバリ出ている。

x-now-deployment-url

Nowはデプロイ時に一意となるIDを発行し、それをURLに利用している (例: goofi-3ejlofj11.now.sh)。productionはgoofi.now.sh固定で良いがstagingのURLは毎回変化するため、この値を動的に取得しないと、SPA側のクライアントからAPI serverへのリクエストを行えない。

Now v1はで環境変数からprocess.env.NOW_URLのように取得できたが、v2からはrequest headerにあるx-now-deployment-urlを参照しなければいけない。

class MyApp extends App<Props> {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    const nowUrl = `https://${ctx.req.headers["x-now-deployment-url"]}`;

    return { pageProps, nowUrl };
  }
}

deprecation warning対応

build時または実行時のログにいくつかのdeprecation warningが見えた。 エラーログにURLが含まれており、リンク先に理由と対処法が書いてあったのでそのとおりに対応した。

https://github.com/zeit/next.js/blob/master/errors/static-dir-deprecated.md https://github.com/zeit/next.js/blob/master/errors/app-container-deprecated.md https://github.com/zeit/next.js/blob/master/errors/no-document-title.md

Now v1 => v2移行は不可逆

一番焦った問題はNow v2にあげてしまうとv1には戻れないという点で、上記の対応をしている間Goofiはずっと止まっていた…。

Unable to deploy to 1.0 · Issue #1805 · zeit/now · GitHub

Goofiは個人開発の趣味アプリケーションなので良かったが、他にも困っている人がおり、うっかり本番で上げてたら死んでた…。

所感

手数が多くて大変だったものの、(特にZEITが志向する)serverlessのパラダイムシフトを感じるupgrade作業だった。

先日のJSConf2019 JPに行くまで知らなかったのだがJAMstackと呼ばれるアーキテクチャがあり、今回の対応もこれに追従するようなもの。

事前にこの対応をしたおかげか、同カンファレンスで行われたGuillermo Rauch*3によるBuilding and deploying for the modern web with JAMstackの内容がより立体的に感じられて良かった。

*1:Now platform上の挙動などを制御する設定ファイル https://zeit.co/docs/configuration/

*2:Now.shもNext.jsも開発元が同一のZEITだから

*3:the founder of ZEIT