はじめに
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を覗いてみます。
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.
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)
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())) }
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を削除しても、そちらを参照するため、データを失わずに済んでいるからです。
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でも動作でき、コンポーネント間のトリガとして利用しているためだと思われます。
https://github.com/openfaas/faas/blob/v0.5/gateway/handlers/alerthandler.go#L16
処理のフローは下記となります。
最後に
今回紹介していない点においてもいくつか興味深い実装がありました。
- Function間を連携させるFunctionチェーンや、wrapper functionを作り、Functionの結果をまとめてからfunctionを実行する方法などもありました。1
- Functionを実行する際の非同期処理が、NATS2で実現が可能です。
今後のロードマップ3 では特権コンテナなどで、Docker BuildやAPI GatewayのKafka対応, NatsQ以外のKafka対応など書かれています。
現状、機能面の自由度が高すぎるため、Authn/Authz,Function間のやりとり、Kubernetes上に存在するFaaS以外のPodへの影響、リソース管理などなど、セキュリティ面、リソース面で問題が数多く残っているように思えますが、個人的な使用という意味では非常に面白い仕組みでした。
Z LabではKubernetesやCNCF周りを中心とした技術調査・開発を行っています。
引き続き、Z Lab Advent Calendar2017をお楽しみください。