エッジサーバにセキュアなJS実行環境を構築できる「Deno Subhosting」を試してみる

## はじめに
みなさんはエッジ関数をつかっていますか？
最近エッジロケーションでJavaScriptの実行環境を提供するサービスが多く登場しています。

仮にエッジ関数を提供するサービスを自分で作ろうとした場合、悪意のあるコードが内部システムに侵入しようとするというようなセキュリティリスクがあります。
このようなセキュリティリスクから守り、セキュアにエッジ関数を提供する機能としてDenoからDeno Subhostingという機能がリリースされました！
本記事ではDeno Subhostingとは何かということを整理し、実際に簡単なサンプルコードを動かしてみます。
## エッジ関数とDeno Subhosting
### エッジ関数
まずはエッジ関数について見ていきましょう。
エッジ関数を提供しているサービスには以下のサービスが挙げられます。
- netlify edge function
- Supabase Edge Functions
- Cloudflare worker
- AWS CloudFront Functions

エッジ関数ではキャッシュよりさらに手前の段階でJSを実行することになります。このメリットとしてはA/Bテストやリクエストに応じたリダイレクトの処理、JWTの検証などを***高速に***行うことができます。

このような考え方は、vercelが提唱する[edge midleware](https://vercel.com/docs/functions/edge-middleware)という概念で整理されています。

https://www.publickey1.jp/blog/23/cdnjspr.html

### エッジ関数を作る上での問題
前述の通り、SaaSサービスとしてエッジ関数を提供しようとした場合、セキュリティ面が極めて重大な懸念事項になります。エッジ関数のプロバイダとしては、ユーザが入力したコードが内部システムに侵入したり、他のユーザーのデータにアクセスするなど脅威から守る必要があります。

### Deno Subhostingとは
Deno Subhostingとは ***「自分で作ったサービス内で、ユーザーによって書かれた信頼されていないJavaScriptコードを安全に実行するプラットフォーム」*** です。Deno Subhostingを用いることでセキュアにエッジ関数を提供できるようになります。

https://deno.com/blog/subhosting

https://deno.com/subhosting/docs/architecture


また、前節で記載したセキュリティリスクから守るために、Deno Subhostingでは以下の三つのアプローチでテナントを隔離しています。(詳細は[こちら](https://deno.com/blog/subhosting-security-run-untrusted-code))
1. APIの制限
javascriptのV8エンジンの機能である[isolate](https://v8docs.nodesource.com/node-0.8/d5/dda/classv8_1_1_isolate.html)での実行、仮想化されたファイルシステムの所有、VPCの分離を通して、自身のランタイムへのアクセスしか許可されないようにしています。[^3]
1. インフラレベルでの防御
独自のプロセス、名前空間、seccompフィルタを使用することで自身のデータにのみアクセスできます。
1. リソースの公平性
cgroupsによるリソース制限やリソース監視ツールの導入により、すべてのユーザーが公平なリソースを利用できることを保証しています。

各レイヤーでの適切な分離と監視により、安全なJS実行環境を提供しているというわけですね。

すでにDeno Subhostingはnetlify edge functionやSupabase Edge Functionsに使用されており、最近ではEC向けプラットフォームを提供しているDeco.cx社でも採用されたようです。


## Deno Subhostingを試す
それでは実際にDeno Subhostingを試してみましょう！

### Quick start
Deno SubhostingのTopの[Get Start](https://dash.deno.com/subhosting/new)から、ページに従ってOrganization、アクセストークン、プロジェクトの順に作成していきます。

OrganizationのIDが表示され、その下の"Generate Access Token"ボタンを押してアクセストークンを生成します。OrganizationIDとアクセストークンが正しいかを確認するテスト用のCurlコマンドを出してくれるので、一応叩いて確認しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/9b71a5ee-6c93-0c2f-cf8f-6acab793fb17.png)
続いてプロジェクトを作成していきます。[公式ページ](https://docs.deno.com/deploy/manual/subhosting/getting_started)にあるように、すでにdenoの環境が整っている方はプロジェクトはjavascriptで書いたコードをdenoコマンドで実行して作成することもできますし、単純にCurlコマンドから作成することも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/7e40a3d7-b7d7-40d6-e1bd-894c2569ed43.png)

```
curl -i -X POST \
  https://api.deno.com/v1/organizations/XXXXXXXXX/projects \
  -H 'Authorization: Bearer XXXXXXX(access_token)' \
  -H 'Content-Type: application/json' \
  -d '{ "description": "My first project" }'
```
プロジェクトの作成までできたら、早速サンプルコードをデプロイしてみます。ここでもcurlとjavascriptでサンプルコードがありますが、今回はcurlでやってみます。内容としてはシンプルなHello Worldのテキストを応答するだけのコードです。
尚、depoyのAPIのパラメーターの説明については[こちら](https://docs.deno.com/deploy/api/rest/deployments)に記載されています。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/a375134b-8c6d-952c-43df-48c218b21db9.png)
```
curl -i -X POST \
  'https://api.deno.com/v1/projects/XXXXXXXXXX/deployments' \
  -H 'Authorization: Bearer XXXXXXX' \
  -H 'Content-Type: application/json' \
  -d '{
    "entryPointUrl": "main.ts",
    "assets": {
      "main.ts": {
        "kind": "file",
        "content": "Deno.serve((req: Request) => new Response(\"Hello World\"));"
      }
    },
    "envVars": {},
    "description": "My first deployment"
  }'
```
正常に終了すると以下のような画面となり、URLが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/751edcd0-4df5-f47f-fe5f-61da47ae514d.png)

URLを叩いてみると、想定通りHello Worldが応答されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/ea9f3677-cb88-9ae2-990c-8dfd90793aec.png)

### デプロイするコードを変更する
先ほどはサンプルのコードをデプロイしてみましたが今度はデプロイするコードを変更してみます。
curlコマンドのリクエストボディ内のmain.tsのcontentsがデプロイされますので、適宜この項目を修正すればよさそうです。
```
"content": "Deno.serve((req: Request) => new Response(\"Hello World\"));"
```
```
"content": "Deno.serve((req: Request) => new Response(\"hogehoge\"));"
```
デプロイコマンドを実行して発行されたDeployment URLを押下すると、表示された文字が変更されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509239/8be155e4-771b-861e-afb1-758271d41aa8.png)

このようにデプロイしたいコードはリクエストのボディに入れることになるので、より複雑なコードをデプロイする際には制御文字の排除等を行うようにしましょう。

### ドメインを取得する
先ほどまででコードのデプロイをcurlからしていましたが、deploy APIのレスポンスにはデプロイ先のドメインが入っていません。
```curl:deploy APIのリクエスト
{
  "id": "XXXXXXXXX",
  "projectId": "XXXXXXXXXXX-XXXXXXXXX",
  "description": null,
  "status": "pending",
  "databases": {},
  "createdAt": "2023-12-17T11:47:11.893397Z",
  "updatedAt": "2023-12-17T11:47:11.893397Z"
}
```
そのためエンドポイントを確認するにはダッシュボードを見に行く必要があると思っていましたが、以下のようにAPIを叩くことでも確認できるようです。{deploymentId}には先ほどのdeploy APIのレスポンスに載っていたidを入れます。
``` curl:リクエスト
curl -i -X GET \
  'https://api.deno.com/v1/deployments/{deploymentId}' \
  -H 'Authorization: Bearer XXXXXXXX' \
  -H 'Content-Type: application/json'
```
``` curl:レスポンス
{
  "id": "XXXXXXXXX",
  "projectId": "XXXXXXXXXXX-XXXXXXXXX",
  "description": "My first deployment",
  "status": "success",
  "domains": [
    "cool-fly-21-qwtrp42em2cx.deno.dev"
  ],
  "databases": {},
  "createdAt": "2023-12-17T11:47:11.893397Z",
  "updatedAt": "2023-12-17T11:47:11.893397Z"
}
```
レスポンスの項目"domains"に先ほどのデプロイ先のドメインが入っています。APIからでも確認できていそうですね！
他にもDeno Deploy REST APIとして多くのAPIが提供されているので気になる方は見てみて下さい。

## まとめ
エッジ関数とは何かという説明からはじめ、Deno Subhostingの紹介と簡単な使用方法について解説しました。

今回はエッジ関数をつくるという文脈でDeno Subhostingを紹介しましたが、ユーザーにアプリ内でカスタムロジックを書く機能を提供する、という視点で考えると他にもいろいろ応用範囲が広がりそうです。
他にもこんな使い方があるよーというアイディアを持っている方がいればぜひ教えてください！