OpenFaaS on a Kubernetes clusterの仕組み

# はじめに
OpenFaaSをKubernetes上で動作させた際、Functionがどのように作成され、実行されているかを見てみます。

# OpenFaaS on a Kubernetes cluster(faas-netes)コンポーネント一覧
- API gateway
    - FaaS REST API + GUI
    - K8Sの場合は、faas-netesdへのProxyもしくは、Docker Swarmの場合は、同サーバ上で処理します
- faas-netesd
    - FaaS本体
    - OpenFaaSに外部プラグインが追加され、K8SのAPIと連携します
    - functionのCRUD操作や実行をします
- Prometheus
- AlertManager
- nats-queue-worker
    - FaaSを非同期実行時させる場合に利用します
    - 使わない場合はすべて同期実行となります
    - 本記事では扱いません

# Functionの管理方法
OpenFaaSでは専用のストレージは持っておらず、K8Sのコンポーネントで管理されているのが特徴です。

## Functionはdeploymentとserviceで表現される
Functionをデプロイすると、namespaceを特に指定しなければ、default namespaceでデプロイされます。
デプロイしたFunctionがK8SのDeploymentとServiceで管理されていることが確認できます。

```
$ kubectl get deployment
NAME            DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
alertmanager    1         1         1            1           1h
faas-netesd     1         1         1            1           1h
gateway         1         1         1            1           1h
hello-python3   1         1         1            1           43m
nodejs-echo     1         1         1            1           1h
prometheus      1         1         1            1           1h
ruby-echo       1         1         1            1           1h
shrink-image    1         1         1            1           1h
url-ping        1         1         1            1           1h

$ kubectl get po
NAME                             READY     STATUS    RESTARTS   AGE
alertmanager-77b4b476b-qdbl5     1/1       Running   1          1h
faas-netesd-64fb9b4dfb-mqss5     1/1       Running   1          1h
gateway-5cb4f64656-pltpb         1/1       Running   1          1h
hello-python3-5997c5598f-jzh5n   1/1       Running   1          43m
nodejs-echo-5c9699c8b5-4zkht     1/1       Running   1          1h
prometheus-7fbfd8bfb8-kv98c      1/1       Running   1          1h
ruby-echo-8d95f97fb-4wkm7        1/1       Running   1          1h
shrink-image-5dd9c4cc7c-qhmq2    1/1       Running   1          1h
url-ping-5965bc9f4f-2gltw        1/1       Running   1          1h

$ kubectl get svc
NAME            CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
alertmanager    10.0.0.79    <nodes>       9093:31113/TCP   6d
faas-netesd     10.0.0.115   <nodes>       8080:31111/TCP   6d
gateway         10.0.0.75    <nodes>       8080:31112/TCP   6d
hello-python3   10.0.0.66    <none>        8080/TCP         44m
kubernetes      10.0.0.1     <none>        443/TCP          6d
nodejs-echo     10.0.0.102   <none>        8080/TCP         1h
prometheus      10.0.0.127   <nodes>       9090:31119/TCP   6d
ruby-echo       10.0.0.64    <none>        8080/TCP         1h
shrink-image    10.0.0.119   <none>        8080/TCP         1h
url-ping        10.0.0.178   <none>        8080/TCP         1h
```
Serviceを見ると、各Functionがポート8080で受けていることがわかります。
一つのfunctionのserviceを見てみます。

```
$ kubectl get svc url-ping -o yaml | grep -a2 selector
    protocol: TCP
    targetPort: 8080
  selector:
    faas_function: url-ping
  sessionAffinity: None
```
labelが `faas_function: url-ping` のPodを対象としていることがわかります。
ここで、faasのCLIでbuildされたDockerfileを覗いてみます。

```:Dockerfile
FROM python:3-alpine

# Alternatively use ADD https:// (which will not be cached by Docker builder)
RUN apk --no-cache add curl \
    && echo "Pulling watchdog binary from Github." \
    && curl -sSL https://github.com/openfaas/faas/releases/download/0.6.9/fwatchdog > /usr/bin/fwatchdog \
    && chmod +x /usr/bin/fwatchdog \
    && apk del curl --no-cache
    …
```
このfwatchdogが8080ポートで受けて、各言語のプロセスを起動しているようです。
fwatchdogは標準入力を子プロセスに渡し、子プロセスの標準出力を受け取るHTTPサーバです。
>The watchdog provides an unmanaged and generic interface between the outside world and your function. Its job is to marshal a HTTP request accepted on the API Gateway and to invoke your chosen appliaction. The watchdog is a tiny Golang webserver - see the diagram below for how this process works.
>![68747470733a2f2f63616d6f2e67697468756275736572636f6e74656e742e636f6d2f363163313639616235636430313334366263336463376131316564633164323138663062653362342f363837343734373037333361326632663730363237333265373437373639366436373265363336663664326.jpeg](https://qiita-image-store.s3.amazonaws.com/0/100376/fe7f640d-ef41-a62f-8864-ed54612c84e6.jpeg)
>:octocat:https://github.com/openfaas/faas/tree/master/watchdog

子プロセスを起動しているだけなので、カスタマイズすれば、言語ランタイムやDocker in Dockerにも対応できそうですね。

## Function(Deployment)をどうやってデプロイしているか?
FaaS-netesのdeploy handlerで、K8S APIへアクセスし、deploymentおよびserviceを作成していることがわかります。

> 		deploy := clientset.Extensions().Deployments(functionNamespace)
>
>		_, err = deploy.Create(deploymentSpec)
>		if err != nil {
>			log.Println(err)
>			w.WriteHeader(http.StatusInternalServerError)
>			w.Write([]byte(err.Error()))
>			return
>		}
>
>		log.Println("Created deployment - " + request.Service)
>
>		service := clientset.Core().Services(functionNamespace)
>		serviceSpec := makeServiceSpec(request)
>		_, err = service.Create(serviceSpec)
>:octocat: https://github.com/openfaas/faas-netes/blob/0.3.7/handlers/deploy.go#L83

Update時もupdate handlerで同様にK8S APIへアクセスし、deploymentを更新しています。
>		if _, updateErr := clientset.ExtensionsV1beta1().Deployments(functionNamespace).Update(deployment); updateErr != nil {
>			w.WriteHeader(http.StatusInternalServerError)
>			w.Write([]byte(updateErr.Error()))
>		}
>:octocat: https://github.com/openfaas/faas-netes/blob/0.3.7/handlers/update.go#L72

※service, deploymentで管理されているため、コンテナイメージが何であれ、ラベルのみ設定し、K8Sへデプロイすると、GUIにそのまま反映されます。

## Functionの試行回数をどうやって管理しているか?
ここまででわかる通り、OpenFaaS自体では永続ストレージは保持しません。
ただ、API Gaway,各Functionやfaas-netesのPodを削除しても、再生成時にはFunction試行回数などは残ったままです。
これは、保存先がPrometheusのメトリクスとして保存されており、Podを削除しても、そちらを参照するため、データを失わずに済んでいるからです。
:octocat: https://github.com/openfaas/faas/blob/v0.5/gateway/server.go#L38

## Functionをどうやってスケールさせているか?
特定のFunctionに対して、API Gateway経由でのリクエスト数が一定数を超えると、Functionはスケールアウト（Podを増やす）します。
この時、API Gatewayが該当するfunctionへのリクエストメトリクスを集め、Prometheusが回収、AlertManagerでFaaSに通信を行うという処理となっています。
これは、AlertManagerをDocker SwarmでもK8Sでも動作でき、コンポーネント間のトリガとして利用しているためだと思われます。
:octocat: https://github.com/openfaas/faas/blob/v0.5/gateway/handlers/alerthandler.go#L16

処理のフローは下記となります。

![scaleout.png](https://qiita-image-store.s3.amazonaws.com/0/100376/5a6bb4ad-002b-4d86-6bc8-f0e5203bd078.png)


# 最後に
今回紹介していない点においてもいくつか興味深い実装がありました。

- Function間を連携させるFunctionチェーンや、wrapper functionを作り、Functionの結果をまとめてからfunctionを実行する方法などもありました。[^1]
- Functionを実行する際の非同期処理が、NATS[^2]で実現が可能です。

今後のロードマップ[^3] では特権コンテナなどで、Docker BuildやAPI GatewayのKafka対応, NatsQ以外のKafka対応など書かれています。
現状、機能面の自由度が高すぎるため、Authn/Authz,Function間のやりとり、Kubernetes上に存在するFaaS以外のPodへの影響、リソース管理などなど、セキュリティ面、リソース面で問題が数多く残っているように思えますが、個人的な使用という意味では非常に面白い仕組みでした。

[^1]: https://github.com/openfaas/faas/blob/b32a3f30ea936215ba10628fb202f7dcb2415e96/guide/chaining_functions.md
[^2]: https://github.com/openfaas/faas/blob/master/guide/asynchronous.md
[^3]: https://github.com/openfaas/faas/blob/master/ROADMAP.md

[Z Lab](https://zlab.co.jp)ではKubernetesやCNCF周りを中心とした技術調査・開発を行っています。
引き続き、[Z Lab Advent Calendar2017](https://qiita.com/advent-calendar/2017/zlab)をお楽しみください。