サーバレスな Oracle Functions (Fn) をやってみた

# はじめに

[Fn Project](https://github.com/fnproject) とは、Oracleがオープンソース化した、コンテナネイティブな サーバレスプラットフォームです。

AWSのLambdaのように、ある程度のまとまりをもった関数をサーバレスに実行できるプラットフォームとなっています。Fnの基本的な動作としては、Functionを呼び出すと、Fn Server上でコンテナを起動し、関数処理を行った後にコンテナを停止するものとなっています。

Oracle Cloud では、Oracle Functions という名前で、Limited Available としてリリースされています。まだ、GAとしてリリースはされていません。Oracle Functions を始めとしたいくつかのサービスを、GA前に触ることができます。下のURLで申し込みが可能です。

申し込み
https://go.oracle.com/LP=78019?elqCampaignId=179851

Oracle Cloud での Cloud Native を紹介
https://www.oracle.com/jp/cloud/cloud-native/



Fn Project のアーキテクチャについては、以下の クラスメソッドさんのブログがわかりやすくでおすすめです。
https://dev.classmethod.jp/server-side/faas-first-step-fn



以下に、Oracle Functions の QuickStart を実行するための手順を書いていきます。Limited Available の時の手順なので、GAされた時とは手順が異なる場合があるのでご注意ください。



# OCI の設定

## リージョンの選択

OCI上で、Phoenix region を選択します。Limited Available時点では、Phoenix region でのみサービスが提供されています。



## VCN

VCNやSubnetを作成します。サービスコンソール上で、VCNを作成するときに、「CREATE VIRTUAL CLOUD NETWORK PLUS RELATED RESOURCES」を選択すると楽で良いです。



## IAM User, Groupの作成

適当に作成します。Oracle Functions を動作するときに、作成した User を利用します。

```
oci iam user create --name sugi --description sugi
oci iam group create --name sugigroup --description sugigroup
oci iam group add-user --group-id ocid1.group.oc1..aaaaaaaahukhfkkc5otxku7zvkhjbhdife7plimfs6kijpej6oqrr77f2bja --user-id ocid1.user.oc1..aaaaaaaaebu2knmup5iqeq4xz6ae2phdlu4vvth423cernryn6s7g7pz3iiq
```



## Compartmentの作成

適当に作成します。すでに作成している場合はそれを利用して良いです

```
oci iam compartment create --name sugicompart --description "myself compartment" --compartment-id <parent_compartment_ocid>
```



## ポリシーの設定

IAM で使用するポリシーを設定します。



- グループと Oracle Functions サービスが Registry (OCIR) を利用する

```
Allow group sugigroup to manage repos in tenancy
Allow service FaaS to read repos in tenancy
```



- グループが作成したコンパートメント内のそれぞれのリソースを利用する

```
Allow group sugigroup to manage functions-family in compartment sugicompart
Allow group sugigroup to manage vnics in compartment sugicompart
Allow group sugigroup to inspect subnets in compartment sugicompart
```



- Oracle Functions サービスが作成したコンパートメント内のリソースを利用する

```
Allow service FaaS to use virtual-network-family in compartment sugicompart
```







# Fn Client の設定

Oracle Functionを操作するときには、fn cli を使用します。fnを動かすにはいくつか条件があります。

- oci cli をinstall
- docker をinstall
- fn cli を install



## oci cli を install

以下の記事をみて適当にinstallします

https://qiita.com/sugimount/items/63a8cfe1163030ae8804

なお、私の環境は、oci cli の Profile名はFnとしています。Profile名は、Fn cli の設定で必要となるので、覚えておきます。

```
> cat ~/.oci/config
[Fn]
key_file=~/.oci/fn.pem
region=us-phoenix-1
tenancy=<ocid>
user=<ocid>
fingerprint=<fingerprint>
```



## Docker の導入

適当にぐぐって導入します
https://docs.docker.com/install/



## Fn cli の導入

fn cli (Goバイナリ)をinstallします

```
curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh
```



Fnバージョンの確認をします

```
> fn version
Client version is latest version: 0.5.51
Server version:  ?
```



## Fn cli で context のコンフィグ

fn cli では、様々な Fn 環境を扱うことが出来ます。例えば、Oracle Functionsの環境、オンプレミスの環境、AWSで稼働させている環境を、一つのcliコマンドで扱うことが可能です。扱う環境を切り替えるために、fn cli 上で context という概念が存在します。
Oracle Functions を扱うための context を設定していきます。

context の作成

```
fn create context sugi-context --provider oracle
```



context一覧の確認

```
> fn ls contexts
CURRENT NAME            PROVIDER        API URL                 REGISTRY
        default         default         http://localhost:8080
        sugi-context    oracle
```



新しく作成した context を使用します

```
fn use context sugi-context
```



context 一覧を確認すると、アスタリスクマークが頭に表示されています

```
> fn ls contexts
CURRENT NAME            PROVIDER        API URL                 REGISTRY
        default         default         http://localhost:8080
*       sugi-context    oracle
```



Oracle Cloud Infrastructure 上で Compartment の OCID を確認し、context へ設定を行います

```
fn update context oracle.compartment-id <compartment-ocid>
```



context に OCI の Endpoint を指定します。

```
fn update context api-url https://functions.us-phoenix-1.oraclecloud.com
```



context 一覧が更新されます

```
> fn ls contexts
CURRENT NAME            PROVIDER        API URL                                         REGISTRY
        default         default         http://localhost:8080
*       sugi-context    oracle          https://functions.us-phoenix-1.oraclecloud.com
```



context に Docker Registory を登録します。Oracle Cloud では、OCIR と呼ばれる Registory サービスを提供しており、これを利用します。
なお、Repositoryは好きな名前を入力することが出来ます。事前に作成する必要はありません。Functionを動作させると、自動的に作成されます。

```
fn update context registry phx.ocir.io/<tenancy-name>/oracle-function
```



context 一覧が更新されます。

```
> fn ls contexts
CURRENT NAME            PROVIDER        API URL                                         REGISTRY
        default         default         http://localhost:8080
*       sugi-context    oracle          https://functions.us-phoenix-1.oraclecloud.com  phx.ocir.io/<tenancy-name>/oracle-function
```



context に、OCI CLI で設定した Profile の名前を指定します。Fn という名前で作成しているため、Fnを指定します

```
fn update context oracle.profile Fn
```



oci cli を使用して、IAM User に紐づいた Auth Token を生成します。OCIRサービスへLoginするときのパスワードとして使用します。

```
oci iam auth-token create --user-id ocid1.user.oc1..aaaaaaaaebu2knmup5iqeq4xz6ae2phdlu4vvth423cernryn6s7g7pz3iiq --description oracle-fuction
```



実行例

```
> oci iam auth-token create --user-id ocid1.user.oc1..aaaaaaaav3ox2f6kvkovbbxbj52hxow2yuub6224vib7n6jvemctuypabt6q --description oracle-fuction
{
  "data": {
    "description": "oracle-fuction",
    "id": "ocid1.credential.oc1..aaaaaaaad5uz5zahynxdobe4udw7e4x6zonaoj5wyp43x3ezlbtagcxb5kzq",
    "inactive-status": null,
    "lifecycle-state": "ACTIVE",
    "time-created": "2019-02-16T01:35:03.427000+00:00",
    "time-expires": null,
    "token": "secret",            <========== ここに表示される文字列を使う
    "user-id": "ocid1.user.oc1..aaaaaaaav3ox2f6kvkovbbxbj52hxow2yuub6224vib7n6jvemctuypabt6q"
  },
  "etag": "d77772be5d2915605d93e2104cc28d9e0fd85ff4"
}
```



OCIR へ Loginします。-u の引数には、適当にテナント名とIAM USER名を指定してください。

```
sudo docker login phx.ocir.io -u <tenancy name>/<iam user name>
```

上記コマンドを実行すると、password 入力を求められます。password は、生成した token の secret を指定します。

プロンプトに `Login Succeeded` と表示されればOKです。

なお、fn cli で設定した context は、yamlファイルとして保存されているため、これを管理することで設定の復元が容易に出来ます

```
> ls -la /home/ubuntu/.fn/contexts
total 16
drwxr-xr-x 2 ubuntu ubuntu 4096 Feb 17 08:01 ./
drwxr-xr-x 3 ubuntu ubuntu 4096 Feb 17 00:59 ../
-rw-rw-r-- 1 ubuntu ubuntu   62 Feb 17 00:59 default.yaml
-rw-rw-r-- 1 ubuntu ubuntu  248 Feb 17 02:39 sugi-context.yaml
```

```
> cat sugi-context.yaml
api-url: https://functions.us-phoenix-1.oraclecloud.com
oracle.compartment-id: ocid1.compartment.oc1..snip
oracle.profile: Fn
provider: oracle
registry: phx.ocir.io/<tenancy name>/oracle-function
```

# Functionの作成

Oracle Function上でアプリケーションを作成します。まずは、何も作成されていないことを fn コマンドで確認します

```
> fn list apps
No apps found
NAME    ID
```



Oracle Functionのコンソール画面を開きます。現在は、Limited Available なので、直接URLを指定してアクセスします。
https://console.us-phoenix-1.oraclecloud.com/functions/
コンソール画面で、適当に Application を作成します (name : hello-fn-go)

作成後、fn コマンド上でも Application の存在を確認することが出来ます

```
> fn list apps
NAME            ID
hello-fn-go     ocid1.fnapp.oc1.phx.aaaaaaaaafjkul4guz7ua5djclxicq7bqwwpcyatiy5fzg4oivcqncujw4aa
```



作業用ディレクトリを作成します

```
mkdir ~/fn_work/
cd ~/fn_work/
```



fn init コマンドで、Golangの 初期function を生成します

```
fn init --runtime go helloworld-func
```



作業ディレクトリ直下に、function名のディレクトリが作成されています。

```
> ls -la ~/fn_work/
total 0
drwxrwxrwx 1 sugimount sugimount 512 Feb 16 14:22 ./
drwxr-xr-x 1 sugimount sugimount 512 Feb 16 14:19 ../
drwxr-xr-x 1 sugimount sugimount 512 Feb 16 14:22 helloworld-func/
```



この中には、functionとして動作させるGolangのソースコードを含めたいくつかのファイルが生成されています。

```
> ls -la ~/fn_work/helloworld-func/
total 0
drwxr-xr-x 1 sugimount sugimount 512 Feb 16 14:22 ./
drwxrwxrwx 1 sugimount sugimount 512 Feb 16 14:22 ../
-rw-r--r-- 1 sugimount sugimount 127 Feb 16 14:22 Gopkg.toml
-rw-r--r-- 1 sugimount sugimount 469 Feb 16 14:22 func.go
-rw-r--r-- 1 sugimount sugimount 113 Feb 16 14:22 func.yaml
```



Golang のソースコードを確認します。ポイントは以下の3点です (なぜ若干複雑な無名Structを使っているのか・・・もうちょっとSimpleな形で良いとおもう)

- `github.com/fnproject/fdk-go` をimportして、main関数内でmyHandler という名前の自作関数を紐づけている
- msg変数は、無名structを代入している。無名structには、Msgフィールドの初期値として、Sprintf関数の返り値である `Hello World`が入る。
- myHandler は、`out io.Writer` にJSONエンコードしたmsg変数を渡すことで、JSONをResponseしている

```golang

> cat func.go
package main

import (
        "context"
        "encoding/json"
        "fmt"
        "io"

        fdk "github.com/fnproject/fdk-go"
)

func main() {
        fdk.Handle(fdk.HandlerFunc(myHandler))
}

type Person struct {
        Name string `json:"name"`
}

func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
        p := &Person{Name: "World"}
        json.NewDecoder(in).Decode(p)
        msg := struct {
                Msg string `json:"message"`
        }{
                Msg: fmt.Sprintf("Hello %s", p.Name),
        }
        json.NewEncoder(out).Encode(&msg)
}
```



fn deploy コマンドで、DockerImageをBuildし、OCIRにPushします。このDockerImageを使用して、OCI側の Fn Server で Function として実行されます。
カレントディレクトリに存在している func.yaml に定義されている内容を基に、deployを行う挙動となります。fn deploy コマンドを発行するたびに、DockerImageのTagがカウントアップしていきます (0.0.1, 0.0.2, 0.0.3 ...)
作成したアプリケーション名を引数に、fn deployコマンドを実行します。

```
fn --verbose deploy --app hello-fn-go
```



なお、今回のチュートリアルで作成されたイメージは、7.28MB とかなり小さいイメージとなっていました。(言語によって大きさが変わりそう)



fnコマンドでアップロードした関数を呼び出します。アプリケーション名とFunction名を指定します。初回実行は30秒ほど待機時間が必要でした。Fn Server 側でdocker pullや docker run に時間がかかっているのかなと想定できます。 2回目以降の実行は、0.3秒といった短い時間で返ってきます。

```
fn --verbose invoke hello-fn-go helloworld-func
```



実行例

```
> fn --verbose invoke hello-fn-go helloworld-func
{"message":"Hello World"}
```



なお、fnの機能で Hot Function という機能があり、実行が終わったコンテナをすぐに停止せず、一定時間稼働させたまま待機させるものがあります。これを活用すると、イイカンジに Function の実行時間短縮が期待できそうです。

参考URL : http://chiroito.hatenablog.jp/entry/2017/10/15/220427





# Memo : Fn Project がサポートしている言語

以下の言語がサポートされているようです (fn init の option で選択できるものから抜粋)

- Go
- Java
- Node.js
- Python
- Ruby
- Kotlin



# Memo : 言語による DockerImageの容量確認

fn init で初期関数を生成したものを使用して、各種言語のDockerImageの大きさを見てみます。単純な興味です。

- Go : 7.28MB (最小)
- Java : 103.26 MB  (大きいけど許容範囲)
- Node.js :  23.48 MB 
- Python : 50.7 MB 
- Ruby : 15.92 MB 
- Kotlin : 104.19 MB  (大きいけど許容範囲)



やはり JVM系は容量が大きいですが、許容範囲だと思います。

```
# Go
cd /home/ubuntu/fn_work
fn init --runtime go helloworld-func
cd helloworld-func
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func

# Java
cd /home/ubuntu/fn_work
fn init --runtime java11 helloworld-func-java
cd helloworld-func-java
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func-java  # 約12秒 (参考にならなそう)

# nodejs
cd /home/ubuntu/fn_work
fn init --runtime node helloworld-func-node
cd helloworld-func-node
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func-node  # 約4秒 (参考にならなそう)

# python 
cd /home/ubuntu/fn_work
fn init --runtime python3.7.1 helloworld-func-python
cd helloworld-func-python
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func-python  # 約6秒 (参考にならなそう)

# ruby
cd /home/ubuntu/fn_work
fn init --runtime ruby helloworld-func-ruby
cd helloworld-func-ruby
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func-ruby  # 約4秒 (参考にならなそう)

# kotlin
cd /home/ubuntu/fn_work
fn init --runtime kotlin helloworld-func-kotlin
cd helloworld-func-kotlin
fn --verbose deploy --app hello-fn-go
time fn --verbose invoke hello-fn-go helloworld-func-kotlin  # 約4秒 (参考にならなそう)
```


# Memo FunctionのResponseを普通な文字列で返却

io.Writerの write func を実行すればよい

```go
func funcHandler(ctx context.Context, in io.Reader, out io.Writer) {
    out.Write([]byte("text message wo kaku"))
}
```


# 参考URL

Getting Start
https://www.oracle.com/webfolder/technetwork/tutorials/infographics/oci_faas_gettingstarted_quickview/functions_quickview_top/functions_quickview/index.html

Cloudii Blog
https://cloudii.atomitech.jp/entry/20190214/1550122678

[FaaS] はじめてのfn #fnproject
https://dev.classmethod.jp/server-side/faas-first-step-fn/

Doker for WindowsでFn Projectを動かしてみた
http://yasu7ri.hatenablog.com/entry/2018/09/28/020223

Hot Function でファンクションのレスポンス時間を短縮 @ Fn
http://chiroito.hatenablog.jp/entry/2017/10/15/220427