Goofiの利用しているNow.shをv1からv2に移行し、その過程でNext.js versionも7.0.2から9.1.1へupgradeした。 Now.shもNext.jsもしばらく全く追いかけていなかったのでやや苦労した。
now\.shのv1 -> v2移行とNext\.jsのv7 -> v9移行を同時にやり、死んだ
— ohbarye (@ohbarye) 2019年10月26日
変更内容は 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); };
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
https://t.co/2IMe5LYglk が昨夜からひっそりと死んでいたけどなんとか蘇った
— ohbarye (@ohbarye) 2019年10月27日
Next v7 -> v9、Now v1 -> v2のアップグレードで事故っていました
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だから