supabaseのEdgeFunctionを使ってみたお話

## はじめに
みなさん、supabaseというサービスをご存知でしょうか？
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/572e3afa-a6c5-0a17-5fba-fdb416de1438.png">
本記事では、supabaseとは？というところから、EdgeFunctionを通じ環境構築、機能利用、CLI接続などなど、、、実際に動かしながらご説明できればと思います。

## supabaseとは
supabaseは、オープンソースのBackend as a Service（BaaS/バース）プラットフォームです。
BaaSとは簡単に言えば、バックエンド機能を簡単に利用できるクラウドサービスです。利点としてはやはり簡単に始められる、というところ。フロント側のアプリケーション開発を重視したい場合など、迅速な開発が可能になります。

代表的なBaaSとしては、今回紹介するsupabaseの他、FirebaseやAWS Amplify、Azure App Serviceなどが挙げられます。
（AWS Amplify、Azure App ServiceどちらもSPAの静的デプロイでしか利用したことなかった。。）

supabaseが提供している機能としては、PostgreSQLを基盤とするデータベース、リアルタイム機能、認証、ストレージなど様々あります。
料金体系については、$0〜利用可能です。本記事は無料範囲内での利用を想定しています。メモリやストレージをアップさせるには追加料金で利用可能となります。

詳しい料金についてはこちら→[supabase公式　料金体系](https://supabase.com/pricing)

:::note warn
無料版は一週間利用がないと、停止状態となります。
停止状態の場合、外部よりアクセスが不可となります。再開するにはsupabaseダッシュボードにログイン後、リストアが必要です。サイズにもよりますが、リストアには数分がかかります。
:::

## supabaseでプロジェクトを作成してみる

では早速、supabaseへアクセスし、便利なバックエンド機能というものを実感してみましょう。

supabaseのサイトへアクセスします

https://supabase.com/

はじめにOrganizationを作成します。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/83eba106-10d1-a2e3-851a-07574a942060.png">

作成したOrganization配下にProjectを作成します。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/ec30f670-96de-dc71-f6c3-25fd3676886e.png">

するとダッシュボートが表示されます。Project名の横の作成中くるくるが
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/d8289c3e-6580-bf08-9bad-a5c4f3b116ef.png">
↓
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/72233dab-4540-d613-72f4-7b095979de80.png">

この様にステータスがグリーンであればOKです。


## CLIでsupabaseへアクセスしてみる
supabase CLIを使用してローカル環境からアクセスしてみます。
参照：[supabase DOCS -CLI-](https://supabase.com/docs/guides/local-development/cli/getting-started#installing-the-supabase-cli)

まずははじめにCLIをbrewでインストールしていきます。
```bash 
brew install supabase/tap/supabase
```

次に個人アクセス用のトークンを利用して、ローカル環境とsupabaseを接続します。

<img width="90%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/f7375823-c138-7ee4-457e-441bf13eed1d.png">

Enterで自動的にsupabaseのページへ遷移します。
ここでアクセストークンが発行され、ローカル環境と接続されます。

:::note warn

アクセストークンはセキュリティ上の重要な情報です。取り扱いには注意してください。
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c0f60bdc-0024-e583-04a9-b1159e594927.png">
:::

オプションでトークン名を指定し、アクセストークン作成、接続も可能です
```bash 
supabase login –name “トークン”
```
次に実際に接続できるようになったのか、見てみましょう。
Supabaseで作成されたプロジェクト一覧を表示するコマンドを実行してみます。

```bash 
supabase projects list
```

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/2f82a6bc-950b-b9b1-bb81-734c776f7518.png">

先ほど作成したプロジェクトが一覧で表示されていればOKです！


ローカル環境のプロジェクト作成する箇所にて初期化処理をします。
今回Vscodeにてフォルダを作成し、以下コマンド実行します。

```bash 
supabase init
```
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/a40011c4-18ac-b57a-4c85-3d94755ad9c5.png">


:::note info
VScodeで開発を行う場合、上記画像の様にコマンド実行後
```
Generate VS Code settings for Deno? [y/N]
```
という質問がされます。supabaseは"Deno"🦕というJS/TSランタイムで作成されています。（Node.jsの制作者が作成したセキュリティ面を改良したもの）そのためローカル環境で利用する際、自動的にDenoの設定も同時に行うか、という質問です。ありがたく`y`入力で自動設定してもらいます。
:::


## EdgeFunctionを作成してみる
次に、supabaseのEdge Functionsを使用し、サーバーレスな関数を作成してみます。

参照：[supabase DOCS -Edge Functions-](https://supabase.com/docs/guides/functions)

supabaseのダッシュボードの左側ドロワーよりEdgeFunctionを選択し、遷移します。
初めて作成する場合、以下の様な画面が表示されます。（すでにFunctionがある場合、`Deploy a new function`のボタンより同様画面が表示されます。）

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b3deb96b-a6f2-8593-29c0-10be11e7a63b.png">

実際のダッシュボード上では、FUNCTION_NAMEは`hello-world`、REFERENCE_IDは自身の作成したプロジェクトに紐づくIDが表記されています。
FUNCTION_NAMEは新規で作成したい名前を任意で入力し、
REFERENCE_IDはsupabaseのProject Settings > Generalからでも確認できます。

実際にCLIより関数`hello-world`を作成してみます。
```bash
supabase functions new hello-world
```
自動生成される関数はこの様な関数です。シンプルなDenoを用いた通信です。

```typescript:supabase>function>hello-world>index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts"

console.log("Hello from Functions!")

Deno.serve(async (req) => {
  const { name } = await req.json()
  const data = {
    message: `Hello ${name}!`,
  }

  return new Response(
    JSON.stringify(data),
    { headers: { "Content-Type": "application/json" } },
  )
})
```
:::note info
Vscodeでの実装時、Denoのエラーが出る場合があります。
Vscodeの拡張機能より”Deno”をインストールをすることで解消します。
:::


----
まずは、ローカル環境で実行してみます。
ローカルのsupabase開発環境を起動するコマンドを実行します。Dockerのコンテナが立ち上がり、認証IDなどが生成されます。supabaseで利用できるサービスすべてをローカル起動させるため、少々時間かかります。
```bash 
supabase start
```
完了するとStudio URLや認証キーであるANON_KEYなどが表示されます。（この後API実行時利用します。）
もし再度表示したい場合は
```bash 
supabase status
```
で同様の設定値を再度確認できます（ただしローカルのsupabase環境が起動している場合のみ）

次に起動したローカル環境で関数を実行するコマンドを実行します。ホットリロード機能付きです。嬉しいね。
```bash 
supabase functions serve
```

これでローカル環境にEdgeFunctionを起動することができました！Postmanを用いて、早速POSTしてみましょう！
Headerに先ほど確認したANON_KEYを`Bearer {ANON_KEY}`として付与し、BodyにJSON形式で値を投げます。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/80229e3e-bf37-5991-98fd-0fb507f0e647.png">

Bodyに渡した値に”Hello”とついて返却されているのが確認できました！やった！！

コンソールログ上にはログも確認できました！
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/fc58d209-6dd8-cf35-9a9c-5c59e42678eb.png">

認証をスキップしたい場合、以下のようなコマンドで実行することで、Headerに認証情報がなくても接続確認を行うことができます。開発時にはとても役立ちそうだ。。！（デプロイコマンドでも`--no-verify-jwt`オプションは利用可能。）
```bash
supabase functions serve hello-world --no-verify-jwt
```

最後にローカル環境停止することをお忘れ無く、、！
```bash 
supabase stop
```
---
次に、Supabaseクラウドへデプロイし、実行してみます。
```bash 
supabase functions deploy hello-world --project-ref {REFERENCE_ID}
``` 
REFERENCE_IDを用いて、プロジェクトの紐付けを行います。REFERENCE_IDはsupabaseのProject Settings > Generalからでも確認できます。
deployを実施すると、supabaseダッシュボード上に関数`hello-world`が作成されています。（反映には少々時間かかります。）

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/87bb2b3c-3eb0-796b-2141-a7fa5cbca2a6.png">

これでsupabase上にEdgeFunctionを起動することができました！ローカル同様、Postmanを用いて、早速POSTしてみましょう！
ヘッダーには以下情報を付与し、呼び出します。
```bash 
'Authorization: Bearer {SUPABASE_ANON_KEY}'
'Content-Type: application/json'
```

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b0f95a18-cc02-4b71-4999-5e9ce2fe7132.png">
Bodyに渡した値に”Hello”とついて返却されているのが確認できました！やった！！

ログはsupabaseダッシュボードよりEdgeFunction>hello-world>Logsより確認できます。
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/d713ddb5-1f7a-6588-627f-abbe8b36651e.png">

## EdgeFunctionでからsupabaseのTBL処理をしてみる。
supabaseには他にもDatabaseの機能を存在します。
今回作成したEdgeFunctionからDB操作をしてみましょう。上記同様公式Docsを参考にしております。

先ほど作成した`index.ts`または、新しい関数としてファイルを新規作成し、`index.ts`に以下関数を記述します。
今回は新しい関数として`select-name`を作成しました。

```typescript:supabase>function>select-name>index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { createClient } from "npm:@supabase/supabase-js"

console.log("Hello from Functions!")

const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!)

Deno.serve(async (req) => {
  const { data,error } = await supabase.from(`message`).select(`name`).limit(1).single()
  console.log(error)
  const message = {
    message: `Hello ${data?.name}!`,
  }
  
  return new Response(
    JSON.stringify(message),
    { headers: { "Content-Type": "application/json" } },
  )
})
```
今回環境変数値として、URLとANONKEYを指定しています。
EdgeFunctionではデフォルトの環境変数値が設定されており、そちらを利用します。
今回は`SUPABASE_URL`と`SUPABASE_ANON_KEY`を利用します。
詳細は[Supabase Docs-Managing Environment Variables-](https://supabase.com/docs/guides/functions/secrets#default-secrets)


もしENVファイルにて、環境変数を定義する場合、ローカルDBへ接続する場合とクラウドDBへ接続する場合でそれぞれ異なります。
ローカルDBへの接続の場合、環境変数値は以下の様に定義します。
```bash 
NEXT_PUBLIC_SUPABASE_URL = http://host.docker.internal:54321/
NEXT_PUBLIC_SUPABASE_ANON_KEY = {ローカル環境のAPIのANON_KEY}
```
URLはローカルサーバーへのURLとします。
（コマンド`supabase status`では`API URL: http://127.0.0.1:54321`と出るのですが、こちらの場合は`TypeError: error sending request for url`エラーとなります。公式より同様質問があり、そちらを[参照](https://github.com/orgs/supabase/discussions/14169#discussioncomment-6303936）)）
ANON_KEYはコマンド`supabase start`によるローカル実行後、`supabase status`にて確認できます。

クラウドDBへの接続の場合、環境変数値は以下の様に定義します。
```bash 
NEXT_PUBLIC_SUPABASE_URL = https://{REFERENCE_ID}.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = {クラウド環境のAPIのANON_KEY}
```
REFERENCE_IDはSupabaseのProject Settings > Generalから確認できます。
ANON_KEYはSupabaseのAPI Settings > Project API Keys > anonから確認できます。

:::note info
ENVファイルで環境変数値を定義する場合、頭に`NEXT_PUBLIC_`の記載が必要です。
公式より同様質問があり、そちらを[参照](https://github.com/orgs/supabase/discussions/3218#discussioncomment-2021448）)
:::

---

まずはローカルDBでの接続を行ってみましょう。コマンド`supabase functions serve`にてローカル環境にEdgeFunctionを起動し、Postmanを用いて、POSTしてみます。
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/cb5c6cc0-000f-252d-3b31-9381aef436a0.png">

この様に呼び出せることはできましたが、Select結果は`undefined`として返ってきました。
。。まだテーブル定義していないので当たり前ですね。

ここで便利な機能。ローカルホストでsupabaseStudioを利用することができます。
つまり、ローカル環境でのダッシュボードとしてGUIで視覚的にデータを作成したり、確認することができるのです。

<img width="90%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/2f62c513-1dd0-9bb5-c920-f4af99bda23d.png">

（少し左側ドロワーの項目が違うのがわかりますね）
このTableEditorよりテーブルを作成してみましょう。
今回はTBL名を"message"とし、カラム"name"を追加してみます。
<img width="90%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/d5cf269e-a64f-bf46-d1b6-1bf3c755b18c.png">

テーブルが作成できたら、データを追加してみます。

<img width="90%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/8c8785bc-d543-516c-edf4-c3296e8bbbfb.png">

また同時にRLS(Row Level Security)も設定します。
RLSとはPostgreSQLでのテーブル内の行(Row)単位でのアクセスを制御する機能です。
今回は自動生成される、すべてのユーザーが閲覧のみ可能なRLSを作成し、付与します。

<img width="90%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/4bd1352e-0958-c86c-01a2-7cc86406b230.png">

さて、これで再度、API呼び出ししてみると、、？

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b2808663-3784-8b26-5028-10b2b3071fa3.png">

この様にDBから値を取ってきて、呼び出すことができる様になりました！

さらにさらに、supabaseへデプロイをしてみると、、？

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c8777b90-b237-1fa6-e89f-dd887ecc01b3.png">

無事にDBから値を取得し、APIで受け取ることができました！

## Terraformで環境を作成してみる
次にIaCツールの1つであるTerraformを用いて、supabase環境を作成してみようと思います。つまり先ほどまでsupabaseのダッシュボードからポチポチ作成していたProjectなどなど、、インフラの構成をソースコードとして管理することができるのです。
参考：[Supabase DOCS -Terraform-](https://supabase.com/docs/guides/deployment/terraform/tutorial)

---
まずはterraformのインストールから始めます。
```bash
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
```
参考：[Terraform公式 Install Terraform](https://developer.hashicorp.com/terraform/install)

:::note warn
brew でインストールの方法について
頭ごなしにコマンド`brew install terraform`を叩かない様に！現在こちら非推奨コマンドです。

現在推奨は`tap`で新しいリポジトリを作成し、そこにインストールする方式をとっています。
参照；[【最新Mac対応】M1、M2、M3チップでTerraformをわずか3分でインストール！](https://zenn.dev/take_tech/articles/32188cd3607721)
:::

さて、実際にTerraformを使ったsupabaseのセットアップをしていきましょう。

まずProviderの設定を行います。
Providerとは、Terraformと接続するクラウドサービスやSaaSと連携するためのプラグインのことです。ここではSupaBaseを指します。

以下ファイルを作成します。
```hcl:module/provider.tf
terraform {
  required_providers {
    supabase = {
      source  = "supabase/supabase"
      version = "~> 1.0"
    }
  }
}

provider "supabase" {
  access_token = file("${path.cwd}/access-token")
}
```
supabaseへアクセスできる様に`access-token`が必要となります。
ダッシュボードのAccount＞Access Tokensにて`Generate new token`ボタンより、新しいアクセストークンを取得し、保存します。一度しか表示されないので、コピーしてローカルのaccess-tokenファイルへ保存します。

次にResourcesの設定を行います。
Resourcesとは、インフラ構成を定義を記述するものです。ここではsupabaseに新しくプロジェクトを作成するという構成を定義します。

以下のファイルを作成します。
```hcl:module/resource.tf
# Create a project resource
resource "supabase_project" "production" {
  organization_id   = {"組織ID"}
  name              = {"作成したいプロジェクト名:ex,tf-example"}
  database_password = {"DBのパスワード"}
  region            = {"作成するリージョン:ex,ap-southeast-1"}

  lifecycle {
    ignore_changes = [database_password]
  }
}

```
`organization_id`はsupabaseのダッシュボードにて、作成したOrganizationのGeneral＞General settings＞Organization slugより確認できます。

これを以下Terraformコマンドで初期化＋実行してみると、、？

```bash
# 初期化
terraform -chdir=module init
# 実行
terraform -chdir=module apply
```
ダッシュボードを開いてみると、新しいプロジェクトが立ち上がっているではありませんか、、！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/1584214e-6af1-8040-1987-f7aa6eaaec7e.png)

新しくTerraformで作成したプロジェクトについては既存プロジェクトの参照IDと紐づけることによって、管理可能となります。
以下の様にプロジェクトIDを定義する変数を追加し、紐づけられる様修正します。

```hcl:module/resource.tf
# ~~追加~~
# Define a linked project variable as user input
variable "linked_project" {
  type = string
}

import {
  to = supabase_project.production
  id = var.linked_project
}
# ~~以降そのまま~~
# Create a project resource
resource "supabase_project" "production" {
  organization_id   = {"組織ID"}
  name              = {"作成したいプロジェクト名:ex,tf-example"}
  database_password = {"DBのパスワード"}
  region            = {"作成するリージョン:ex,ap-southeast-1"}

  lifecycle {
    ignore_changes = [database_password]
  }
}
```
TerraformのVariablesは環境変数値など変更できるパラメータを指します。
この状態でapplyコマンドを実行すると、
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/82b989d0-d39c-4f16-4a5c-b57598d1f92b.png">
この様にコマンド実行ごとにプロジェクトIDを入力し、実行が可能となります。

また、実行ごとに入力するのも億劫なので、ファイルに別途定義を記載しておけば、そこから値を引っ張ってくることも可能です。

```hcl:module/terraform.tfvars
linked_project = {"プロジェクトID"}
```

## まとめ
今回、EdgeFunctionを通じ、さまざまなsupabaseの機能を利用することができました。思ったより簡単に利用できますし、ローカルでもGUIで操作できたりするのも良いところです。また今回初めてDenoというランタイムに触れましたが、そちらも簡単に利用できたのも良かったと思います。しかしsupabaseはそれでもまだまだ利用しきれていない機能が盛りだくさんなので、引き続き利用してみたいと思います。


### 参考
コマンド苦手な私が、大変助かったCLIコマンド一覧を最後に参照させていただきます。今後の自分自身が利用するためのメモがわりです。

https://note.com/libproc/n/n232eaded8ca6