[FaaS] はじめてのfn #fnproject

2018.10.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こむろ@事業開発部です。訳あってFaaSのサービス実装について調べています。

はじめに

fnは Function as a Service(FaaS) の実行基盤を構築するサービス実装の一つです。Oracleが開発を主導しており、現在OSSでの開発が進められています。

FaaSの基本的な動作としては、Functionを呼び出すごとにContainerが起動され、仕事が終わったら一定の生存期間後に縮退、大きなComputer Resourceを複数のContainer(実態はFunction)で共有しながら待機時間を減らし効率の良い処理を目指します。

おや?これはAWS Lambdaなのでは?

LambdaもFaaSの一つになります。

違いはComputer Resourceの管理、運用がAWS側の管理下にあり、我々利用者側の制御管轄外であることです。当然ながら自前で管理する他のFaaSサービス実装は、それぞれComputer Resourceの管理・運用についても自分たちで制御しなければなりません。

しかし反面、細かい制御を行うことができるというところが大きい違いになりそうです。また特定のクラウド環境に縛られずに、動作する環境を選択できるのもメリットの一つではないでしょうか。

Feature

fn projectはOracleが開発を行っており、現在Open sourceでGithub上で活発に開発が行われています。

fn projectはいくつかのComponentで提供しており、主となるComponentはFunctionの実行環境の fn-server となります。これらのComponentをまとめたものを総称としてfn projectという名前を冠しているようです。

数多くのComponentがRepository上に公開されていますが、代表的なものをあげてみました。

サービス名 説明 Repository URL
fn(fn-server) fnはFunctionを実行・管理するためのServerアプリケーションである。Goで記述されており、起動するとDocker ImagをPullし実行。fn-serverプロセスが立ち上がる。 https://github.com/fnproject/fn
fdk 各言語に対応したfn用のSDK。Go, node, Java, Python, Ruby等が現時点で確認できる言語となっている。 https://github.com/fnproject/fdk-go 等・・
cli fnをインストールすると同時に導入される。fn-serverへのDeployやFunctionの作成・管理等々のfnに関わる作業をすべてこちらのCLIから実行することができる。 https://github.com/fnproject/cli
lb fn-serverをクラスタで利用する際に必要なLoad Balancerサービス。起動しているfn-serverプロセスを1ノードとして管理し、それぞれを追加・削除することでユーザーから要求されたFunctionの実行を分散する。 https://github.com/fnproject/lb
flow Workflowを構成するサービス。単体で動作するFunctionをWorkflowに従ってまとめることでより複雑なタスクを実行することができる(と思う。未調査) https://github.com/fnproject/flow
ui fn-serverのFunction実行状況や、Deploy済みFunction等々の情報を視覚化することができるWebサービス。管理用。 https://github.com/fnproject/ui

この中で最小限動作に必要なものは fn, fdk, cliの3つです。

それ以外のComponentはスケールが必要な環境の構築や本番運用時に必要になるツールのようです。ツールやサーバーはほぼGoで記述されており、コード量は比較的少ないように見えます。

Dockerベース

fnはDockerのContainer技術をコアにサービスが構成されており、Docker Image Repository(もしくはそれに準拠するRepository)を利用することができます。つまり、Docker CompatibilityであればECR等のサービスでも利用が可能です。

公式には以下のような特徴が記載されています。

  • Open Source
  • Native Docker: use any Docker container as your Function
  • Supports all languages
  • Run anywhere
    • Public, private and hybrid cloud
    • Import Lambda functions and run them anywhere
  • Easy to use for developers
  • Easy to manage for operators
  • Written in Go
  • Simple yet powerful extensibility

興味深いのがLambdaのConvert機能を有している点です。コマンドでLambdaのARNを指定するとfnのFunctionへの変換が可能とのこと(どこまで可能なのか試してみたい)

fnの目指すところ、fnの基本的な考え方や機能については Medium - 8 Reasons why we built the Fn Project に詳しく記載されています。

必要な最低限のスペック

Dockerのバージョンなど最低限必要なサービスやスペックは以下の通りです。

  • Docker 17.10.0-ce or later installed and running
  • A Docker Hub account (Docker Hub) (or other Docker-compliant registry)
  • Log Docker into your Docker Hub account: docker login

dockerのバージョンは 17.10.0-ce 以降のバージョンが必要です。そのため、これ以下のバージョンでは動作しません。

FaaSにおいて実行すべきFunctionのQueueingのためのサービス、Functionのメタ情報を格納するためのデータベースが必須となっていますが、fnの場合はローカルで動作するデータベースが組み込まれているため(SQLiteやインメモリキュー)、ローカルでの実行は簡単です。

Try fn

fnではFDKと呼ばれるSDKが提供されており、いくつかの言語に対応している。それぞれの言語のTutorialがあり、シンプルなFunctionを作成するだけならばドキュメントに沿っていくだけでコードの記述、Deploy、実行までが可能です。Hello World Functionの作成から実行までを確認してみます。

First Function

fnはSQLiteとインメモリのキューイングによりローカルで動作させることが可能です。

前提条件

  • DockerがインストールされておりDaemonが起動している状態
  • macOS Sierraで検証

Install

fn-cliを導入。

$ brew install fn

Macの場合は brew がお手軽。Linuxの場合やbrewが実行できない場合は以下のコマンドで fn をインストールします。

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

https://github.com/fnproject/fn#install-cli-tool

Start fn-server

Functionはfn-serverによって管理され実行されます。

fn-server自体もDocker Imageから起動されるコンテナであり、Imageが存在しない場合は起動と共にfn-serverのImageをPullした上で起動されます。

$ fn start
time="2018-10-19T01:59:48Z" level=info msg="Registering container driver 'docker'"
time="2018-10-19T01:59:48Z" level=info msg="Registering log provider 's3'"
time="2018-10-19T01:59:48Z" level=info msg="Registering data store provider 'sql'"
time="2018-10-19T01:59:48Z" level=info msg="Registering log provider 'sql'"
time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'mysql'"
time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'postgres'"
time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'sqlite'"
time="2018-10-19T01:59:48Z" level=info msg="Setting log level to" level=info
time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="Connecting to DB" url=/app/data/fn.db
time="2018-10-19T01:59:48Z" level=info msg="datastore dialed" datastore=sqlite3 max_idle_connections=256 url="sqlite3:///app/data/fn.db"
time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3"
time="2018-10-19T01:59:48Z" level=info msg="agent starting cfg={MinDockerVersion:17.10.0-ce DockerNetworks: DockerLoadFile: FreezeIdle:50ms EjectIdle:1s HotPoll:200ms HotLauncherTimeout:1h0m0s AsyncChewPoll:1m0s MaxResponseSize:0 MaxLogSize:1048576 MaxTotalCPU:0 MaxTotalMemory:0 MaxFsSize:0 PreForkPoolSize:0 PreForkImage:busybox PreForkCmd:tail -f /dev/null PreForkUseOnce:0 PreForkNetworks: EnableNBResourceTracker:false MaxTmpFsInodes:0 DisableReadOnlyRootFs:false DisableTini:false DisableDebugUserLogs:false IOFSEnableTmpfs:false IOFSAgentPath:/iofs IOFSMountRoot:/Users/komurohiraku/.fn/iofs IOFSOpts:}"
time="2018-10-19T01:59:48Z" level=info msg="no docker auths from config files found (this is fine)" error="open /root/.dockercfg: no such file or directory"
time="2018-10-19T01:59:48Z" level=info msg="available memory" cgroupLimit=9223372036854771712 headRoom=268435456 totalMemory=1688129536
time="2018-10-19T01:59:48Z" level=info msg="sync and async ram reservations" availMemory=1419694080 ramAsync=1135755264 ramAsyncHWMark=908604211 ramSync=283938816
time="2018-10-19T01:59:48Z" level=info msg="available cpu" availCPU=2000 totalCPU=2000
time="2018-10-19T01:59:48Z" level=info msg="sync and async cpu reservations" cpuAsync=1600 cpuAsyncHWMark=1280 cpuSync=400
time="2018-10-19T01:59:48Z" level=info msg="Fn serving on `:8080`" type=full

        ______
       / ____/___
      / /_  / __ \
     / __/ / / / /
    /_/   /_/ /_/
        v0.3.566

これでFunctionをDeployし実行する環境が整いました。検証時のfnのバージョンは v0.3.566になります。続いてFunctionの方を作成していきましょう。

fn-server側の動作ログも確認したいので、こちらの起動した画面はそのままにしておいてください。

Create Initial function from template

対応言語を指定するだけで、その言語で記述されたFunctionの雛形を作成できます。Getting Startedに従って実行してみます。

$ fn init --runtime go dummy

上記コマンドはGoのFDKを利用したサンプルアプリケーションが生成されます。Function名はdummy を指定しました。dummy/ が作成され必要なファイルが作成されます。

$ tree dummy/
dummy/
├── Gopkg.toml
├── func.go
└── func.yaml

Functionの実装は func.go に。Functionの設定メタデータは func.yaml に記載されています。

schema_version: 20180708
name: dummyfunc
version: 0.0.1
runtime: go
entrypoint: ./func
format: http-stream

versionはdeploy実行ごとに0.0.1ずつインクリメントされていく模様です。Application名と同じにしてしまうとちょっと後半に困るので nameのみ dummyfunc に変更しました。

Edit Sample Function

生成されたFunctionの内容を確認してみます。

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)
}

fdk-go を利用しています。所謂Hello Worldの簡単なFunctionコードのようです。goについてはあまり詳しくないのですがなんとなくやっていることが分かりそうです。

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

jsonの name キーを取得してPerson.Nameに値を入力するようです。

p := &Person{Name: "World"}

入力値に name キーを持ったJsonが指定されている場合はJsonの値を、存在しない場合は "World" という文字列が指定されると読めます。

もう一つの Gopkg.toml というファイルはGoの依存関係管理ツールの設定ファイルのようです。Developers.IO - Goオフィシャルチーム作成の依存関係管理ツール dep を試してみた

Deploy Function

Docker RepositoryへのImageのPushをスキップしてLocalに直接FunctionをDeployできます。これによりDocker Repositoryアカウントなどを必要とせずに簡単にFunctionを実行することができます。

$ fn --verbose deploy --app dummy --local
Deploying dummy to app: dummy
Bumped to version 0.0.2
Building image dummy:0.0.2
FN_REGISTRY:  FN_REGISTRY is not set.
Current Context:  No context currently in use.
Sending build context to Docker daemon   5.12kB
Step 1/10 : FROM fnproject/go:dev as build-stage
dev: Pulling from fnproject/go
ff3a5c916c92: Already exists
f32d2ea73378: Pull complete
3bdfb30a4c89: Pull complete
6487ee6212c5: Pull complete
074903419fc0: Pull complete
3db945ee2177: Pull complete
Digest: sha256:6ebffaea00a2f53373c68dd52e0df209d7e464d691db0d52b31060d06df8e839
Status: Downloaded newer image for fnproject/go:dev
 ---> fac877f7d14d
Step 2/10 : WORKDIR /function
 ---> Running in 9af29a96f8bb
Removing intermediate container 9af29a96f8bb
 ---> fa81f6a83538
Step 3/10 : RUN go get -u github.com/golang/dep/cmd/dep
 ---> Running in 7254aebcb478
Removing intermediate container 7254aebcb478
 ---> 28053ba3fdcb
Step 4/10 : ADD . /go/src/func/
 ---> f255d62a2e97
Step 5/10 : RUN cd /go/src/func/ && dep ensure
 ---> Running in e9a06a1092ba
Removing intermediate container e9a06a1092ba
 ---> a325e3f6bbc1
Step 6/10 : RUN cd /go/src/func/ && go build -o func
 ---> Running in 6c8085d20384
Removing intermediate container 6c8085d20384
 ---> dbb08c9e6fb6
Step 7/10 : FROM fnproject/go
latest: Pulling from fnproject/go
1eae7a7426b0: Pull complete
7a855df78530: Pull complete
Digest: sha256:8e03716b576e955c7606e4d8b8748c0f959a916ce16ba305ab262f042562340f
Status: Downloaded newer image for fnproject/go:latest
 ---> 76aed4489768
Step 8/10 : WORKDIR /function
 ---> Running in 019de7b9bfc7
Removing intermediate container 019de7b9bfc7
 ---> bea8b592cfbd
Step 9/10 : COPY --from=build-stage /go/src/func/func /function/
 ---> 11e135a40c50
Step 10/10 : ENTRYPOINT ["./func"]
 ---> Running in ad71de97bb06
Removing intermediate container ad71de97bb06
 ---> c02608a82b7d
Successfully built c02608a82b7d
Successfully tagged dummy:0.0.2

Updating function dummy using image dummy:0.0.2...
Successfully created app:  dummy
Successfully created function: dummy with dummy:0.0.2

--verbose 指定のためかなり詳細に出ていますが、依存するImageをPullしながらfunctionの実行コードを含んだImageを作成し、fn-server側へ登録しています。

Execute Function

Functionの実行にはいくつかあります。

CLIで実行する

fnコマンドのinvokeでDeployしたFunctionを実行します。

DeployしたApplication名とFunctionの設定メタデータに記載したFunction名を指定します。

$ fn invoke dummy dummyfunc
{"message":"Hello World"}

invokeの後ろの引数はDeployしたアプリケーション名, メタデータに記述したFunction名 になります。

実行引数を付与してFunctionを実行する

Functionの実行時に実行引数を付与して任意のパラメータをFunctionにわたすことができます。

$ echo -n '{"name":"KOMURO"}' | fn invoke dummy dummyfunc --content-type application/json
{"message":"Hello KOMURO"}

パイプで渡すことができるようです。

HTTPメソッドで実行する

DeployされたFunctionは特定のエンドポイントを持ちます。そのため、このエンドポイントを通してHTTPメソッドでFunctionを呼び出すことができます。こちらも実行引数をつけて呼び出しが可能です。

$ curl -H "Content-Type: application/json" -d '{"name":"kom"}' -X GET 'http://localhost:8080/t/dummy/dummy-trigger'
{"message":"Hello kom"}

Content-Typeに application/json を指定してInputにJSONを指定しています。起動しているfn-serverの方では以下のログが出力されています。

time="2018-10-19T05:10:38Z" level=info msg="starting call" action="server.handleHTTPTriggerCall)-fm" appName=dummy app_id=01CT53JT4XNG8G00GZJ0000001 container_id=01CT5BQ223NG8G00GZJ000000M fn_id=01CT541M8DNG8G00GZJ0000007 id=01CT5BQ223NG8G00GZJ000000K triggerSource=/dummy-trigger

trigger経由で実行されているのが分かります。

ちなみにエンドポイント情報は以下のコマンドで取得可能です。

$ fn list triggers dummy
FUNCTION    NAME        TYPE    SOURCE      ENDPOINT
dummyfunc   dummy-trigger   http    /dummy-trigger  http://localhost:8080/t/dummy/dummy-trigger

注意事項

fnのバージョンアップによって、メタデータに明示的にTriggerという項目を追加しないとHTTPのエンドポイントが追加されなくなったようです。現在、fn-serverのREADMEの修正が追いついていないようで、READMEは参照にするとコケますのでお気をつけください。

エンドポイントを設定させるために func.yaml を修正します。

triggers:
- name: dummy-trigger
  type: http
  source: /dummy-trigger

再度Deployし直すと、HTTPで呼び出せるエンドポイントが作成されます。

以下が以前のバージョンと比べると大きな変更点です。

  • routes というコマンドが存在しない
  • Triggerという概念が追加されている

元々 routes というコマンドが存在しエンドポイントの情報が確認できましたが(v0.3.504あたり)現在はコマンドがなくなっています。さらにREADMEの修正が追いついておらず、実装やHELPを見たほうが早いかもしれません。今のところ(2018/10/19現在)はTutorial(Go)のマニュアルが正しそうです。

まとめ

最小限のFunctionを作成しつつサーバーの起動からFunctionのDeploy、実行までを一通り体感してみました。ローカルで動作させるためのサービスやアプリケーションが組み込まれているため、準備にさほど時間がかからずに体感することができました。

実際にこれができるからイコールで本番適用できるわけではないのですが、検証までの時間をとても短くできることはとても良いかと思います。今後は実際運用できるレベルの構成が可能かなどの検証を引き続き行っていきます。

ただ、困ったことにコマンドを含めて様々な破壊的変更も多く入っているようで、先週まで動いていたものが動かない、といった状況もちょいちょい出てきており、このあたりはまだまだ完成には遠いのかなという印象です。そういったところも含め今後もしっかりウォッチしていこうかと思います。

参照