🐁

貧乏エンジニアリングGopherの強力な武器vercel serverless functions

2023/03/19に公開

はじめに

おはようございます

推しの村山美羽ちゃんによる致死量を超えるかわいい写真の供給が始まったので自動で収集したいという記事を書いた際にvercelserverless-functionsが貧乏エンジニアリングにとって非常に強力なツールとなることがわかりました。
そこで今回はもう少し深掘りしてvercelserverless-functionsを使ってGoサーバーを構築してみたのでご紹介します。

https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/go

version

言わずもがな。

❯ go version
go version go1.19.7 darwin/arm64

vercelを利用するのに必要です。

node --version
v18.15.0

型情報が欲しいのでopenapiを使います。また、コードの自動生成が欲しいのでoapi-codegenを使います。

❯ oapi-codegen -version
github.com/deepmap/oapi-codegen/cmd/oapi-codegen
v1.12.4

準備

vercelCLIを利用するので以下の公式ドキュメントを参考にインストールします。

https://vercel.com/docs/cli

npm install -g vercel
❯ vercel --version
Vercel CLI 28.17.0
28.17.0
❯ vercel login

Vercel CLI 28.17.0
? Log in to Vercel 
● Continue with GitHub 
○ Continue with GitLab 
○ Continue with Bitbucket 
○ Continue with Email 
○ Continue with SAML Single Sign-On 

さまざまな方法でログインができますが私はGitHubにてログインしました。

プロジェクトの開始

とりあえずプロジェクトを作成するために公式ドキュメントにあるコードを用意します。

mkdir <project>
cd <project>
go mod init <project>
mkdir api
touch api/index.go
package api

import (
	"fmt"
	"net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<h1>Hello from Go!</h1>")
}

こちらも公式サイトにある設定を用いてvercel.jsonを用意します。

touch vercel.json
{
  "build": {
    "env": {
      "GO_BUILD_FLAGS": "-ldflags '-s -w'"
    }
  }
}

一旦動作確認のためにデプロイします。
コマンドの実行履歴は以下となります。

> vercel --prod
Vercel CLI 28.17.0
? Set up and deploy “~/Work/<project>”? [Y/n] y
? Which scope do you want to deploy to? <github_account>
? Link to existing project? [y/N] n
? What’s your project’s name? <project>
? In which directory is your code located? ./
Local settings detected in vercel.json:
No framework detected. Default Project Settings:
- Build Command: `npm run vercel-build` or `npm run build`
- Development Command: None
- Install Command: `yarn install`, `pnpm install`, or `npm install`
- Output Directory: `public` if it exists, or `.`
? Want to modify these settings? [y/N] n
🔗  Linked to <github_account>/<project> (created .vercel and added it to .gitignore)
🔍  Inspect: https://vercel.com/<github_account>/<project>/D3CoTPf9zgaD8nhvhc5RGggn1YSH [1s]
✅  Production: https://<project>.vercel.app [16s]
> curl https://<project>.vercel.app/api
<h1>Hello from Go!</h1>

curlを用いて動作することを確認します。
index.goで設定したレスポンスが取得できたことが確認できました。

実装

実践的なサーバーを構築するために

  • 複数エンドポイントの対応
  • 複数メソッドの対応

を実装してみます。

routingについては以下の記事を参照したところvercel.jsonに設定すればできるようです。
(公式ドキュメントで記載を見つけられず。。。)

https://qiita.com/mugi111/items/9063e7c6d9e86164d6c3

最終的に以下のディレクトリ構成にしました。

.
├── api
│   └── v1
│       ├── health
│       │   └── index.go
│       └── users
│           └── index.go
├── internal
│   ├── handler
│   │   ├── health.go
│   │   └── user.go
│   └── openapi
│       └── types.gen.go
├── openapi.yaml
└── vercel.json
  • apiディレクトリ配下にserverless-functionsのエントリーポイントを実装します。(公式に合わせてindex.goを採用しています。)
  • internal/handlerディレクトリ配下に実際の処理を記載しています。
  • internal/openapiディレクトリ配下にopenapi.yamlから自動生成した型を配置しています。

エンドポイントのroutingはディレクトリ構成およびvercel.json設定により実現しています。
実際に用いている設定は以下となります。

{
  "build": {
    "env": {
      "GO_BUILD_FLAGS": "-ldflags '-s -w'"
    }
  },
  "routes": [
    { "src": "/api/v1/health", "dest": "/api/v1/health" },
    { "src": "/api/v1/users", "dest": "/api/v1/users" }
  ]
}

Pathパラメータを利用したところうまくroutingされなかったためQueryパラメータを用いています。

メソッドの分岐は愚直にコード内で記載しています。
index.go(エントリーポイント)内で分岐を行い実際の処理を呼び出すように実装しました。

package users

import (
	"log"
	"net/http"

	"github.com/takokun778/samplegovercel/internal/handler"
)

func Handler(w http.ResponseWriter, r *http.Request) {
	log.Printf("%s %s", r.Method, r.URL)

	switch r.Method {
	case http.MethodGet:
		handler.GetUser(w, r)
	case http.MethodPost:
		handler.PostUser(w, r)
	case http.MethodPut:
		handler.PutUser(w, r)
	case http.MethodDelete:
		handler.DeleteUser(w, r)
	case http.MethodOptions:
		w.Write([]byte(""))
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

動作確認

GET api/v1/health

curl -X GET 'https://<project>.vercel.app/api/v1/health' -i
HTTP/2 200
OK

POST api/v1/users

> curl -X POST -H 'Content-Type: application/json' -d '{"name": "村山美羽"}' 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 200
{"user":{"id":"8c710495-15b9-47d8-8771-52d26f9ffb23","name":"村山美羽"}}

POST api/v1/users(bodyなし)

curl -X POST -H 'Content-Type: application/json' 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 500 

POST api/v1/users(body空)

curl -X POST -H 'Content-Type: application/json' -d '{}' 'https://samplegovercel.vercel.app/api/v1/users' -i
HTTP/2 400

GET api/v1/users?id=1

curl -X GET 'https://<project>.vercel.app/api/v1/users?id=1' -i
HTTP/2 200
{"user":{"id":"1","name":"村山美羽"}}

GET api/v1/users(id指定なし)

curl -X GET 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 404 

PUT api/v1/users?id=1

curl -X PUT -H 'Content-Type: application/json' -d '{"name": "村山美羽"}' 'https://<project>.vercel.app/api/v1/users?id=1' -i
HTTP/2 200 
{"user":{"id":"1","name":"村山美羽"}}

PUT api/v1/users(id指定なし)

curl -X PUT -H 'Content-Type: application/json' -d '{"name": "村山美羽"}' 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 404

PUT api/v1/users(bodyなし)

curl -X PUT -H 'Content-Type: application/json' 'https://<project>.vercel.app/api/v1/users?id=1' -i
HTTP/2 500

PUT api/v1/users(body空)

curl -X PUT -H 'Content-Type: application/json' -d '{}' 'https://<project>.vercel.app/api/v1/users?id=1' -i
HTTP/2 400

DELETE api/v1/users?id=1

curl -X DELETE 'https://<project>.vercel.app/api/v1/users?id=1' -i
HTTP/2 200 
OK

DELETE api/v1/users(id指定なし)

curl -X DELETE 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 404

PATCH api/v1/users

curl -X PATCH 'https://<project>.vercel.app/api/v1/users' -i
HTTP/2 405

注意点

  • go mod vendor を利用するとdeploy時にError: Unexpected error. Please try again later. ()が発生してデプロイされませんでした。
  • npm init -y & npm install --save-dev vercel により環境を汚さない方法も考えましたが、go mod tidyの際にnode_modulesが邪魔になりエラーが発生したのでグローバルにインストールしています。
  • Pathパラメーターでid指定をしたかったがうまくroutingできなかったのでQueryパラメータを利用しています。
  • root配下にindex.goを配置してデプロイしてアクセスするとindex.goファイルがダウンロードされてしまいます。
  • vercel deployコマンド(--prodなし)を実行するとpreview環境へデプロイされいちいち動作確認するのがめんどくさかったので--prodを指定しています。

お片付け

サンプル実装とはいえ、GitHubのアカウントはリポジトリからエンドポイントが推測できてしまうのでお掃除しておきます。

vercel project rm <project-name>

おわりに

https://zenn.dev/takokun/articles/bf37c7669185ed

以前cyclicというサービスを利用して無理やりFaaSにGoサーバーを建てる方法を考えてみましたがvercelを用いてGoサーバーを簡単に構築できました!

https://www.cyclic.sh/

vercel serverless functionsのツボがある程度わかりました。非常に魅力的なサービスです。
CLIを眺めているとdevコマンドなどもありローカル環境に閉じて開発もできそうです。
また、使ってみて知見が溜まったら共有します。

また、vercel serverless functionsはどうやらAWS Lambdaみたいです。

https://vercel.com/docs/concepts/limits/overview#serverless-function-execution-timeout

実装する際にはLambdaを意識しながら実装する必要がありそうです。

  • コードはなるべく小さく(コールドスタートが早くなる)
  • 同時実行数
  • 実行時間の上限
  • ペイロードサイズ
  • コネクション爆発 etc...

今回実装したコードはこちらに置いておきます。

https://github.com/takokun778/samplegovercel

Discussion