Nowを使って静的サイトとAPIを単一レポジトリで運用する

November 10, 2018

以前、Gatsby.jsを使って柏市議会の議事録を解析した結果を公開するサイトを作成しました。 その時の記事です。

そのサイトに、頻出共起単語(要するに文中で一緒に使われることが多い単語)を見れるようにする機能を追加するにあたって、ある単語の頻出共起単語リストを取得するAPIが必要になりました。 現状サイトをデプロイしているNetlifyにはLambda関数を実行できる環境があるので、それを利用して実現は可能です。 ただ、最近知った Now というサービスを使ってみたかったので、静的サイトとAPIまとめてNowに移行してみました。

Now

https://zeit.co/now

Next.jsを開発しているZEITが提供しているサーバーレスのwebサイト|アプリ実行環境です。 デプロイが非常にシンプルで、xxx.jsやxxx.goなどにHTTPリクエストを扱うLambdaを実装して now コマンドを叩くだけでデプロイが完了します。(厳密に言うとLambda実装がphpなのかnodeなのかgoなのか、といったビルドに関する設定は必要ですが)

ソースファイルをNow上でどのように扱うかはファイルごと(あるいは正規表現を使って表現されるファイル群)に定義します。 あるファイル(群)は静的サイトジェネレータを使ってビルド、あるファイルはLambdaに変換してWeb APIとして公開、あるファイル(例えば画像とか)はそのまま公開、といった設定が、一つのプロジェクト内で柔軟に設定でき、モノレポで管理している静的サイトやAPIをNow上にコマンド一発で展開できます。

Now CLIのinstall

公式サイト にinstall方法の記載があります。Desktopアプリもあるようですが、私はCloud9で開発しているので

npm install -g now

でCLIだけinstallしました。

Gatsbyによる静的サイトをNowに展開する

既存のGatsbyによる静的サイトをNowに展開します。

まず、プロジェクトルートに以下の内容をnow.jsonとして記述してください。 now.jsonは設定ファイルです。 builds は、指定のファイル( src )に対してどういったビルド処理を実行するか( use )を指定するための設定です。 static-build を利用すると、 src に指定されたpackage.jsonに定義された now-build を実行した後に、distディレクトリの内容を公開します。

{
  "builds": [
    { "src": "package.json", "use": "@now/static-build" }
  ]
}  

package.jsonの scripts"now-build": "gatsby build && mv public dist" を追加します。 Gatsbyはビルド結果をpublicに吐き出すので、処理完了後renameしています。

あとは now コマンドを実行するだけです。 議事録解析結果を取得するエンドポイントの指定が必要だったので、実行時に引数でビルド時に必要な環境変数を指定します。

now -b API_ENDPOINT="http://gateway.dataforkashiwa.net.s3-website-ap-southeast-1.amazonaws.com"

ビルド時に必要な環境変数は -b で指定します。

初めてNowを利用するときは認証リンクをクリックさせるフローが走るので、指示通りにやってください。

これで、Gatsbyによって生成された静的サイトがNowにデプロイされます。

※特に設定をしない場合、デプロイされたソースや、ビルドや実行時に生成されるログは誰でも閲覧可能な状態になっています。ドキュメント を参考にして、必要に応じて公開されないようにしてください。

Lambda

共起単語を取得するAPIを定義するために、apiディレクトリを作成してcollocations.jsを定義します。

const CollocationData = require('./collocations_data');
const { parse } = require('url');

module.exports = (req, res) => {
  const word = parse(req.url, true).query.word;
  const result = CollocationData.fetchCollocations(word);
  res.writeHead(200, {
    'Content-Type': 'application/json'
  });
  res.write(JSON.stringify(result));
  res.end();
};

collocations_data.jsに、共起単語を取得するための関数を定義します。

const { collocations } = require('./collocations.json');

module.exports = {
  fetchCollocations: (word) => {
    if(!word) { return []; }
    
    const result = collocations[word];
    if(!result) { return []; }
    
    return result;  
  }
};

api/collocations.jsonに予め用意した解析結果のjsonファイルを置きます。

now.jsonの builds に以下を追加。

{ "src": "apis/collocations.json", "use": "@now/node" }

now を実行してデプロイします。 @now/node を使うと、src に定義された関数がNode.js実装のAPIとして公開されます。

これで、 /api/collcations.js?word=xxx にアクセスすると共起単語のリストを返すAPIが公開されます。

routes

この状態だと、 apiディレクトリ以下のファイルを変更しただけなのに、デプロイのたびにGatsbyのビルドも再度走ってしまい無駄です。 apiディレクトリがGatsbyの src に指定したpackage.jsonより下層のディレクトリにあるからと思われます。 これを避けるためには、siteディレクトリを作って、 Gatsby.jsで利用するソースをサブディレクトリに移動させます。

ただ、今度は /site 以下にデプロイされるようになります。静的サイト部分に / からアクセスしたければ、now.jsonに以下を追加してください。

"routes": [
  { "src": "/api/(.*)", "dest": "/api/$1" },
  { "src": "/*", "dest": "/site" }
]

/api 以下へのアクセスはそのまま /api 以下にデプロイされたLambdaへアクセスさせ、それ以外のアクセスは全てGatsby.jsによるビルド結果がデプロイされた /site以下にアクセスさせる設定です。

これで、 /api でLambdaを実行させつつ、静的サイトを / 以下にデプロイすることができます。

テスト

apiにテストを書きたければ、 AVAなどのテストランナーを入れて実行すれば良いです。Lambdaはリクエストとレスポンスの2つを引数に取るので、それぞれ必要なインタフェースをモックするオブジェクトを用意するか、 mock-resを使ってください。 以下一例です。

api/package.json を用意します。

{
  "scripts": {
    "test": "ava"
  },
  "devDependencies": {
    "ava": "1.0.0-rc.2",
    "mock-res": "^0.5.0"
  }
}

テストを api/test.js に実装します。

import test from 'ava';
import MockRes from 'mock-res';
import CollocationData from './collocations_data';
import handler from './collocations';

test('共起単語リストを返す', t => {
  const mockRequest= {
    url: '/collocations.js?word=議会'
  }; 
  const mockResponse = new MockRes();
  CollocationData.fetchCollocations = () => { return ['税金', '市長']; };
  handler(mockRequest, mockResponse);
  const result = mockResponse._getJSON();
  t.is(result.length, 2);
  t.true(result[0] === '税金' && result[1] === '市長');
});

apiディレクトリに移動して npm test でテストを実行することができます。

プロジェクトの構成

以下のような構成になります。(ファイルについては抜粋)

- プロジェクトルート
  - api ... APIとして公開するLambda実装
    - collocations.js
    - package.json
    - test.js
  - now.json
  - .nowignore ... デプロイ時にNowにアップロードしないファイル、ディレクトリを指定するファイル(.gitignoreみたいなもの)
  - site ... Gatsbyによる静的サイトのためのソース
    - gatsby-config.js
    - package.json
    - src
    - ...
comments powered by Disqus