🦕

vercel-denoで自作DenoモジュールをServerless化する

2021/06/27に公開

先日、Denoを使ってZennのAPIを作りました。

https://zenn.dev/kawarimidoll/articles/3d51924dc595b6

これはDenoのプログラムから呼び出して使うことはできるのですが、せっかくなのでDeno以外からもアクセスできるようにしました。

ローカルサーバーを作る

まず、httpでサーバーからアクセスできるようにします。
Deno公式でローカルサーバーを立てる方法が紹介されています。

https://deno.land/manual@v1.11.2/examples/http_server#using-the-codestdhttpcode-library

最もシンプルなサーバーは次のようになります [1] 。同ディレクトリに前回作成したzenn_api.tsがある想定です。

server.ts
import { serve } from "https://deno.land/std@0.99.0/http/server.ts";
import { zennApi } from "./zenn_api.ts";

const port = 8080;
const server = serve({ port });
console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`);

for await (const request of server) {
  const body = JSON.stringify(await zennApi(request.url));
  const headers = new Headers({ "Content-Type": "application/json" });
  request.respond({ status: 200, headers, body });
}

request.urlには相対パスが入ってくるので、それをzennApi()に渡します。
この結果がJSONで帰ってくるので、headers"Content-Type": "application/json"を設定して返却しています。

立ち上げます。

❯ deno run --allow-net server.ts
HTTP webserver running.  Access it at: http://localhost:8080/

別端末からcurlしてみます。

❯ curl localhost:8080             
{"dailyTechArticles":[{"id":41129,"title":"正式仕様リリース! JavaScriptの最新仕様ES2021で追加された新機能まとめ","slug":"es2021-whats-new","published":true,"commentsCount":3,"likedCount":292,"bodyLettersCount":9013,"readingTime":10,"articleType":"tech","emoji":"💐"
(略)
"page":"/","query":{}}

❯ curl localhost:8080/kawarimidoll
{"user":{"id":39895,"username":"kawarimidoll","name":"kawarimidoll","avatarSmallUrl":"https://storage.googleapis.com/zenn-user-upload/avatar/icon_2379ac8d86.jpeg","avatarUrl":"https://storage.googleapis.com/zenn-user-upload/avatar/2379ac8d86.jpeg",
(略)
"page":"/[username]","query":{"username":"kawarimidoll"}}                   

取得できました。このサーバーを公開すれば、外部からのアクセスが可能になります。

Vercelデプロイ用に設定する

今回はvercel-denoを使ってVercelへデプロイします。

https://github.com/TooTallNate/vercel-deno

現在の状態のままではデプロイできないので、いくつか設定を行っていきます。

serverの設定

vercel-denoでServerless関数を実行する際、ローカルサーバーを立てた際のようにrequestを待ったりする処理は必要ありません。
ハンドラだけあれば良いので、READMEの例に従い、先程のserver.tsを整理します。

server.ts
- import { serve } from "https://deno.land/std@0.99.0/http/server.ts";
+ import { ServerRequest } from "https://deno.land/std@0.99.0/http/server.ts";
  import { zennApi } from "./zenn_api.ts";

- const port = 8080;
- const server = serve({ port });
- console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`);

- for await (const request of server) {
+ export default async (request: ServerRequest) => {
    const body = JSON.stringify(await zennApi(request.url));
    const headers = new Headers({ "Content-Type": "application/json" });
    request.respond({ status: 200, headers, body });
- }
+ };

なお、ローカルで実行する場合は、requestをハンドラにわたす処理をdebug.tsに退避しておけば可能です。

debug.ts
import { serve } from "https://deno.land/std@0.99.0/http/server.ts";
import handler from "./server.ts";

const port = 8080;
const server = serve({ port });
console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`);

for await (const request of server) {
  handler(request);
}
❯ deno run --allow-net debug.ts
HTTP webserver running. Access it at: http://localhost:8080/

ディレクトリ構成の修正

VercelのServerless Functionsの制約上、使用するコードをapiディレクトリ配下においておく必要があります [2]

後ほどデプロイしますが、例えばserver.tsをデプロイ項目に指定しても、apiディレクトリ内でないと怒られます。

ということで、実行に必要なファイルをapiディレクトリ配下に移動します。

今回のデプロイとは関係ないけど必要な修正

zenn_api.tsserver.tsapiディレクトリに入るので、これらの相対パスは変化しませんが、前回モジュール公開用に作成したmod.tsはルートに残しておきたいので、このインポートパスを修正します。

mod.ts
- export { ZENN_ROOT, zennApi } from "./zenn_api.ts";
+ export { ZENN_ROOT, zennApi } from "./api/zenn_api.ts";

ローカル実行用にdebug.tsを作った場合も、server.tsのインポートパスの修正が必要になると思います。

debug.ts
- import handler from "./server.ts";
+ import handler from "./api/server.ts";
(略)

Vercelの設定ファイルの追加

vercel.jsonを作成します。

vercel.json
{
  "functions": {
    "api/server.ts": {
      "runtime": "vercel-deno@0.8.0",
      "includeFiles": "api/*.ts"
    }
  },
  "rewrites": [{ "source": "/(.*)", "destination": "/api/server" }]
}

functionsで、api/server.tsのランタイムをvercel-denoに設定しています。また、apiディレクトリ配下の他のファイルを読み込むよう指定しています。

また、デフォルトでは、xxx.vercel.app/apiへのアクセスが自動的にapiディレクトリ以下のファイルに割り振られるようになっています。
どういうことかというと、xxx.vercel.app/api/helloにアクセスするとapi/hello.tsが実行されるような形式です。
しかし今回は、すべてのアクセスをapi/server.tsでさばきたいので、rewritesでその旨を設定します。
ここの記法は正規表現っぽいですが違うので注意してください。

https://vercel.com/docs/configuration

また、このvercel.jsonおよびapi配下以外のファイルはデプロイ時には不要なので、.vercelignoreも追加しておきましょう。

/*
!api
!vercel.json

デプロイに必要な構成の確認

ここまでで、以下のようなファイル構成になっていると思います[3]。以前の記事で作成したmod.tsや、今回確認用に作成したdebug.tsもありますが、.vercelignoreのルールによりデプロイはされません。

❯ tree
.
├── .vercelignore
├── api
│  ├── deps.ts
│  ├── server.ts
│  └── zenn_api.ts
├── debug.ts
├── mod.ts
└── vercel.json

これで準備が完了しました。Vercelからデプロイしてみます。

デプロイする

Vercelのダッシュボードに入りましょう。
GitHubとの連携が必要になります。
https://vercel.com/dashboard

New Projectから…

目的のものをImportします。

Personalを使用します。

特に設定変更は不要です。

デプロイします。

Deploy logの途中で赤文字が出るかもしれません[4]が、最終的に成功の表示が出れば問題ないはずです。
以下のパスでアクセスできるようになります。

https://deno-zenn-api.vercel.app/
https://deno-zenn-api.vercel.app/kawarimidoll
TOPページやユーザーページの内容を取得できました。

おわりに

今回は自作したZennのデータを取得するモジュールをVercelに上げてみました。
何らかのデータをJSON形式で出力するモジュールであればほぼそのまま使えるはずですし、その他の形式にも応用できると思います。

関連プロジェクト

実は、vercel-denoはしばらく前に作ったTypograssyというツールで使っていました。こちらはデータをSVG形式で出力しています。

Typograssyの動作説明はQiitaに書いているのでよろしければご覧ください。
https://qiita.com/kawarimidoll/items/14ce32323715389f1b88

動作説明は残しておいたのですが、実装まわりの手順を完全に忘れていて難儀しました。
ちゃんと作業記録や知見は残しておかないと駄目ですね。
過去の自分のためにも未来の自分のためにも記録をつけておくようにしたいものです。

脚注
  1. Deno1.9からNative HTTP Server APIが提供されていますが、vercel-denoでの実行方法がわからなかったので、ここではstd/httpを使って実装しています。 ↩︎

  2. 他の方法もあるのかもしれませんができませんでした ↩︎

  3. 現実的にはREADMEや.gitignoreもあるはずです ↩︎

  4. Kyのロードでエラーが表示されました ↩︎

Discussion