OpenFaaS を使って API サーバをたててみた

## この記事の目的

Kubernetes を学ぶ途中で出現した OpenFaaS を使ってみます．
本記事では OpenFaaS を利用して API サーバを立ち上げる手順をご紹介します．

:::note info
こちらの記事では OpenFaaS の立ち上げからモックサーバの作成までを行います．
:::

:::note warn
本記事は API サーバを爆速で立ち上げる方法をご紹介するものではありません．
~~OpenFaaS を触ってみることを主眼に置いているので「[json-server](https://github.com/typicode/json-server) とか使った方が早くない？」は禁止です~~
:::

## 構成

https://docs.openfaas.com/architecture/stack/

![openfaas-service.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/94a05351-5b19-d7db-577c-cb34217dedf1.png)

本記事では赤枠部分のサービスを扱います．
OpenFaaS はコンテナビルドをして起動する構成であるため CI/CD との相性は良さそうですが，本記事では扱いません．

![openfaas-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/d9458ff0-a1fa-d75b-441b-2bc600f8a428.png)

ローカルの Minikube を使って一連のコンテナを立ち上げています．

## サービスの紹介

### [OpenFaaS](https://www.openfaas.com/)

![openfaas-logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/047476d4-9887-c89c-bc39-fcea981917ed.png)

> OpenFaaS® makes it easy for developers to deploy event-driven functions and microservices to Kubernetes without repetitive, boiler-plate coding. Package your code or an existing binary in a Docker image to get a highly scalable endpoint with auto-scaling and metrics.

OpenFaaS は Kubernetes 環境上で動作するオープンソースの Function as a Service (FaaS) [^IBM/FAAS]です．

[^IBM/FAAS]: https://www.ibm.com/topics/faas

サーバレス環境を提供することで，開発者が自作のコンテナイメージを利用したバッチサーバや API サーバなどを簡単に立ち上げることができます．
デフォルトで Go, Node.js, Python, PHP 等をサポートしていますが，OCI 準拠のコンテナイメージをあらかじめ作成しておくことで任意の言語にも対応することができます．

一方で，実際はコンテナを立ち上げて実行しているに過ぎないので，基盤目線だとコストや監視などの運用方法を検討したうえで導入することになります．
ここで AWS Lambda[^AWS/LAMBDA] のような「起動した時間分だけ課金する」といったメリットを享受するには多大な工夫が必要に思います．

[^AWS/LAMBDA]: https://aws.amazon.com/lambda/

筆者は，小規模 (何なら自分だけ) の開発や閉じられた環境 (自宅など) でのサービス立ち上げをイメージした時には有力な候補になってくるのかなと考えています．

少しでも興味を持った場合は，[公式のチュートリアル](https://docs.openfaas.com/tutorials/cli-with-node/)も準備されているため Kubernetes 環境さえあれば簡単に試してみることができます．
ドキュメントの参照だけでは心配な方向けに，丁寧な説明がされた eBook (有償) やワークショップ動画など[^OPENFAAS/TRAINING]も準備されているようです．

[^OPENFAAS/TRAINING]: https://docs.openfaas.com/tutorials/training/

なお OpenFaaS の[金額体系](https://www.openfaas.com/pricing/)は
1. Community Edition (無償)
1. OpenFaaS Standard ($1,000/月)
1. OpenFaaS for Enterprises (要相談)
 
の 3 つからなりますが，今回はお試しで利用するので Community Edition を利用します．

## 動作確認環境

動作確認を行った環境情報を以下に示します．

| 名称 | バージョン |
|----|----|
| Windows 11 | 22H2 |
| Ubuntu (WSL2) | 20.04.6 LTS |
| Docker | 20.10.17 |
| Docker Compose | v2.10.2 |
| Kubernetes | v1.28.3 |
| kubectl client | v1.25.0 |
| kubectl kustomize | v4.5.7 |
| minikube | v1.32.0 |
| helm | v3.13.3 |
| OpenFaaS | 0.27.3 |
| faas-cli | 0.16.21 |
| Go | 1.21.5 |

### faas-cli 環境準備手順

OpenFaaS の公式 CLI である [faas-cli](https://github.com/openfaas/faas-cli) を準備しておきます．
公式で準備されているシェルスクリプトを実行 (```curl -sSL https://cli.openfaas.com | sudo -E sh```) すればいいだけですが，個人的な都合で配置場所と環境変数は管理しておきたいので手動で行います．

シェルスクリプトの中身を確認したい場合は ```wget https://cli.openfaas.com -O get.sh``` を利用するとよいと思います．

```shell
# faas-cli の取得
mkdir -p $HOME/extracts/openfaas/bin && mkdir -p $HOME/installers/linux/openfaas && cd $_ && \
wget https://github.com/openfaas/faas-cli/releases/download/0.16.21/faas-cli && \
cp faas-cli $HOME/extracts/openfaas/bin
# パスを通す
vim $HOME/.bashrc
> # faas-cli
> export FAASCLI_HOME=$HOME/extracts/openfaas
> export PATH=$PATH:$FAASCLI_HOME/bin
source $HOME/.bashrc
# インストール確認
faas-cli version
```

## OpenFaaS の起動

以下の記事を参考に作業を進めます．
ポートフォワード, ```faas-cli login``` 等の作業は変更しているので注意してください．

https://faun.pub/getting-started-with-openfaas-on-minikube-634502c7acdf

```shell
# openfaas および openfaas-fn の namespace を作成
kubectl apply -f https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.yml
> namespace/openfaas created
> namespace/openfaas-fn created
# helm レポジトリ追加
helm repo add openfaas https://openfaas.github.io/faas-netes/
> "openfaas" has been added to your repositories
# チャート更新
helm repo update
# パスワード作成
export PASSWORD=$(head -c 12 /dev/urandom | shasum| cut -d' ' -f1)
echo $PASSWORD
# secret 発行
kubectl -n openfaas create secret generic basic-auth --from-literal=basic-auth-user=admin --from-literal=basic-auth-password="$PASSWORD"
> secret/basic-auth created
# OpenFaaS インストール
helm upgrade openfaas --install openfaas/openfaas --namespace openfaas --set functionNamespace=openfaas-fn --set basic_auth=true
# 起動待機
kubectl -n openfaas get pods
> NAME                            READY   STATUS    RESTARTS      AGE
> alertmanager-795bbdc56c-f8qmp   1/1     Running   0             12m
> gateway-67df8c4d4-5bwsc         2/2     Running   0             12m
> nats-5c48bc8b46-56xbz           1/1     Running   0             12m
> prometheus-78d4c9f748-42f5l     1/1     Running   0             12m
> queue-worker-b9965cc56-wvjl9    1/1     Running   3 (11m ago)   12m

# OpenFaaS ログイン用パスワードの取得
LOGIN_PASSWORD=$(kubectl -n openfaas get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode) && echo "OpenFaaS admin password: $LOGIN_PASSWORD"
# ポートフォワード http://localhost:8080 で OpenFaaS 管理 GUI にアクセスできるようになる
## ログインユーザは admin, パスワードは $LOGIN_PASSWORD を使う 
kubectl port-forward -n openfaas svc/gateway 8080:8080
# ポートフォワード http://localhost:8090 で Prometheus GUI にアクセスできるようになる
kubectl port-forward -n openfaas  deployment/prometheus 8090:9090

# faas-cli から OpenFaaS に接続 別ターミナルから
echo -n $LOGIN_PASSWORD | faas-cli login -g http://localhost:8080 -u admin --password-stdin
> Calling the OpenFaaS server to validate the credentials...
> credentials saved for admin http://localhost:8080
```

http://localhost:8080
![openfaas-top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/c907c5e2-8a34-1d6a-507b-9ff7c270578a.png)

http://localhost:8090
![openfaas-prometheus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/9fae9c9f-7c04-3e4a-c31c-a5774a8496f3.png)

## FaaS を試す

### 既存の関数を利用する

一番簡単に FaaS を試すには，公式が提供している既存の関数をそのまま利用するのがよいと思います．
```faas-cli store list``` を叩くことで既存の関数一覧を取得できます．

```shell
faas-cli store list
> FUNCTION              AUTHOR       DESCRIPTION
> nodeinfo              openfaas     NodeInfo
> env                   openfaas     env
> sleep                 openfaas     sleep
> shasum                openfaas     shasum
> figlet                openfaas     figlet
> printer               openfaas     printer
> curl                  openfaas     curl
> external-ip           openfaas     external-ip
> youtube-dl            openfaas     youtube-dl
> sentimentanalysis     openfaas     SentimentAnalysis
> hey                   openfaas     hey
> nslookup              openfaas     nslookup
> certinfo              stefanprodan SSL/TLS cert info
> colorise              alexellis    Colorization
> inception             alexellis    Inception
> alpine                openfaas     alpine
> face-detect-pigo      esimov       Face Detection with Pigo
> ocr                   viveksyngh   Tesseract OCR
> qrcode-go             alexellis    QR Code Generator - Go
> nmap                  openfaas     Nmap Security Scanner
> cows                  openfaas     ASCII Cows
> text-to-speech        rorpage      OpenFaaS Text-to-Speech
> mquery                rgee0        Docker Image Manifest Query
> face-detect-opencv    alexellis    face-detect with OpenCV
> face-blur             esimov       Face blur by Endre Simo
> normalisecolor        alexellis    normalisecolor
> coherent-line-drawing esimov       Line Drawing Generator from a photograph
> openfaas-exif         servernull   Image EXIF Reader
> openfaas-opennsfw     servernull   Open NSFW Model
> identicon             rgee0        Identicon Generator
> chaos                 alexellis    chaos
```

curl 関数を起動してみます．
関数を実行する Pod が起動していることを確認後 ```POST``` で関数を実行します．
curl で API を叩き，curl 関数が ```curl``` を実行します．
(もっとわかりやすい例にすればよかった...)

```shell
# 関数のデプロイ
faas-cli store deploy curl --gateway localhost:8080
> Deployed. 202 Accepted.
> URL: http://localhost:8080/function/curl

# 関数が Pod で起動していることを確認
kubectl -n openfaas-fn get svc
> NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
> curl   ClusterIP   10.97.208.189   <none>        8080/TCP   4m
kubectl -n openfaas-fn get pods
> NAME                    READY   STATUS    RESTARTS   AGE
> curl-5d499476b8-szksq   1/1     Running   0          4m6s

# 関数テスト実行
echo http://worldtimeapi.org/api/timezone/Asia/Tokyo | faas-cli invoke curl --gateway localhost:8080
>   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
>                                  Dload  Upload   Total   Spent    Left  Speed
> 100   343  100   343    0     0   3784      0 --:--:-- --:--:-- --:--:--  3811
> {"abbreviation":"JST",..."week_number":1}

# 関数実行
curl -X POST http://localhost:8080/function/curl -d "http://worldtimeapi.org/api/timezone/Asia/Tokyo"
>   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
>                                  Dload  Upload   Total   Spent    Left  Speed
> 100   343  100   343    0     0   3784      0 --:--:-- --:--:-- --:--:--  3811
> {"abbreviation":"JST",..."week_number":1}
```

OpenFaaS の管理 GUI からも既存関数のデプロイや，起動した関数のテスト実行を行うことできます．

![openfaas_curl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/0ddb855a-9565-5751-d5f6-9a90c9990c8c.png)

表示からわかるようにコンテナイメージは ```ghcr.io/openfaas/curl:latest``` が利用されています．

### テンプレートから自作関数を作成して利用する

テンプレートを利用してビルドしたイメージを利用することで，自作のサーバーレス関数をデプロイすることができます．

現在 OpenFaaS で提供されているテンプレートは 2 種類存在します．

ひとつは[クラシックテンプレート](https://github.com/openfaas/templates)です．

```shell
mkdir -p $HOME/codes/GitHub/openfaas && cd $_ && \
# クラシックテンプレートを取得
faas-cli template pull
```

もうひとつは Template Store で提供しているテンプレートです．

```shell
# テンプレート一覧を確認
faas-cli template store list
# 特定のテンプレートを取得
faas-cli template store pull {template name}
```

これらの違いはアーキテクチャにあり，前者は Classic watchdog，後者は [of-watchdog](https://github.com/openfaas/of-watchdog/blob/master/README.md) を利用しています．
通信方法や繰り返し起動時の挙動が異なり，基本的に of-watchdog の方がより高いパフォーマンスを発揮できるとのことです．

https://docs.openfaas.com/architecture/watchdog/

ここでは Template Store から取得できる [```golang-middleware```](https://github.com/openfaas/golang-http-template) テンプレートを利用して新規関数を起動します．

```shell
# golang-http, golang-middleware テンプレート取得
faas-cli template store pull golang-http
> Fetch templates from repository: https://github.com/openfaas/golang-http-template at
> 2024/01/06 19:51:38 Attempting to expand templates from https://github.com/openfaas/golang-http-template
> 2024/01/06 19:51:40 Fetched 2 template(s) : [golang-http golang-middleware] from https://github.com/openfaas/golang-http-template

ls
> template

# テンプレートから新しい関数用ファイルを作成
faas-cli new --lang golang-middleware echo
> Folder: echo created.
>   ___                   _____           ____
>  / _ \ _ __   ___ _ __ |  ___|_ _  __ _/ ___|
> | | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
> | |_| | |_) |  __/ | | |  _| (_| | (_| |___) |
>  \___/| .__/ \___|_| |_|_|  \__,_|\__,_|____/
>       |_|
> 
> 
> Function created in folder: echo
> Stack file written: echo.yml
> 
> Notes:
> You have created a new function which uses Go 1.21 and Alpine
> Linux as its base image.
> 
> To disable the go module, for private vendor code, please use
> "--build-arg GO111MODULE=off" with faas-cli build or configure this
> via your stack.yml file.
> 
> Learn more: https://docs.openfaas.com/languages/go/

ls
> echo  echo.yml  template
```

作成した ```echo``` 配下で ```go get``` 等を行って Go 開発手順で関数を準備することができます．[^OPENFAAS/GO]

[^OPENFAAS/GO]: https://docs.openfaas.com/languages/go/

本記事ではデフォルト状態のままでビルドしてみます．
デフォルトではリクエストボディを ```("Body: %s", string(input))``` でレスポンスに含める handler が準備されています．

```golang: echo/handler.go
package function

import (
	"fmt"
	"io"
	"net/http"
)

func Handle(w http.ResponseWriter, r *http.Request) {
	var input []byte

	if r.Body != nil {
		defer r.Body.Close()

		body, _ := io.ReadAll(r.Body)

		input = body
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(fmt.Sprintf("Body: %s", string(input))))
}
```

```echo.yml``` を適切に設定します．

```yaml: echo.yml
version: 1.0
provider:
  name: openfaas
  # gateway: http://192.168.49.2:31112
  gateway: http://localhost:8080
functions:
  echo:
    lang: golang-middleware
    handler: ./echo
    image: echo:latest
```

```faas-cli build``` でコンテナイメージをビルドし，DockerHub などのレジストリに push しておきます．

```shell
# 念のため利用するイメージをあらかじめ pull しておく
docker pull golang:1.21-alpine
docker pull alpine:3.18.4
docker pull ghcr.io/openfaas/of-watchdog:0.9.13
# ビルド
faas-cli build -f ./echo.yml
> [0] > Building echo.
> Building: echo:latest with golang-middleware template. Please wait..
> ...
> Total build time: 111.72s

# イメージが準備できていることを確認
docker images
> REPOSITORY                                TAG           IMAGE ID       CREATED         SIZE
> echo                                      latest        33d10630847c   2 minutes ago   21.5MB

# イメージを DockerHub などに push しておく
## イメージ名変更
docker tag echo:latest caunus/openfass-echo:latest
## ログインして push
docker login
docker push caunus/openfass-echo:latest
```

OpenFaaS の管理 GUI から ```caunus/openfass-echo:latest``` を利用した関数を起動してみます．

![openfaas-echo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/47cf62d5-f171-6fec-3ee1-a2bb3f5539a4.png)

![openfaas_echo_invoke.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/c00a0696-e8e0-a64e-61dc-a7257b252751.png)

テスト実行 (INVOKE) でリクエストに含めた ```foo-bar``` がレスポンスに含まれて返されていることがわかります．

```shell
# 関数の起動確認
kubectl -n openfaas-fn get svc
> NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
> curl            ClusterIP   10.97.208.189   <none>        8080/TCP   134m
> openfass-echo   ClusterIP   10.98.52.162    <none>        8080/TCP   6m20s
kubectl -n openfaas-fn get pods
> NAME                             READY   STATUS    RESTARTS   AGE
> curl-5d499476b8-szksq            1/1     Running   0          134m
> openfass-echo-6947cb4976-2j9v2   1/1     Running   0          6m22s

# 関数実行
curl -X POST http://localhost:8080/function/openfass-echo -d "hoge-fuga"
> Body: hoge-fuga
```

実際に ```POST``` してみても正しく関数が動いていることが確認できました．

## まとめ

OpenFaaS を使って(特に Go 製の) API サーバーを立ち上げてみました．
テンプレートを使ってコンテナイメージのビルドが必要になってくるため，組織で使うにはある程度の知識習熟が必要になるサービスかもしれません...
そもそも刺さる人は一部の界隈に限るのかもしれませんが，個人的にはいくらか手元でサービスを立てて管理したいときがあるので活用できる機会はありそうと思いました．

## 後片付け (オマケ)

お試し環境を削除する手順も念のため載せておきます．

```shell
helm uninstall -n openfaas openfaas
kubectl delete namespace openfaas openfaas-fn
```