[FaaS] Fn with Docker Hub #fnproject

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

こむろです。札幌寒い。

はじめに

前回Hello World Functionを作成しLocalサーバーでの実行を行いました。このままではローカルで立ち上げたサーバーで実行できるのみで、新たにサーバーを立ち上げてスケールさせることができません。

FunctionのDocker ImageをRepositoryにPushし、新たにサーバーを立ち上げても同じFunctionを実行できるような環境の構築を目指します。

今回はDocker RepositoryへのPushとImageのPullまでを確認します。

fnは発展途上のOSSです。今回紹介する内容は未来(次の日かも知れない)で正しくなくなる可能性があります。その点を踏まえた上で必要ならばご参照ください。

Docker Hub Repositoryを使ってDeployする

fnはdeployを実行すると、Functionを登録すると同時にFunctionのDocker Imageを作成し、RepositoryへのPushを行います。ImageのRegistryとしてDocker Hubがデフォルトで設定されています。今回はTutorialのCreating a Function from a Docker Imageに沿って作業を行います。そのためデフォルト設定のままImageをDocker HubへPushします。

環境の各種バージョン情報はこちら。

  • Docker Hub Account
  • fn version 0.5.15
  • docker version 18.06.1-ce

What is Context

まずはfnのContext設定をチェックします。Contextとはfnコマンドを実行する上で設定されるClientの環境変数郡のようなもののようです。APIの呼び出し先を指定するAPI URL やDocker Repositoryの設定の REGISTRY といった必須項目が存在するのが確認できます。

Check Context

$ fn list context
CURRENT NAME    PROVIDER    API URL             REGISTRY
*   default default     http://localhost:8999/v1    

ここではCURRENT* がついていることとAPI URL のポート/パス及び REGISTRY を確認します。fn invoke でFunctionを実行する際には、このContextの情報をもとにfn-serverへのリクエストを作成します。

稀に先頭のチェックがついていない場合があり(Contextが指定されていない)、その場合は以下のコマンドでどのContextを利用するかを選択します。今回はdefault Contextを指定します。

$ fn use context default

再度チェックして CURRENT* がついていればOKです。

Update Context

続いてREGISTRY 設定を更新します。fnはDocker HubをImageのRegistryとしてデフォルトで設定されており、特に何もしなければDocker HubへImageのPush, Pullを行うようになっています。Docker Hubの場合、REGISTRY にはDocker Hubのログインユーザー名を登録します。

update コマンドを利用し、Docker Hubのログインユーザー名を登録します。

$ fn update context registry hogeuser
Current context updated registry with hogeuser

hogeuser は適宜自分のアカウント等で差し替えてください。再度Contextを確認します。

$ fn list context
CURRENT NAME    PROVIDER    API URL             REGISTRY
*   default default     http://localhost:8999/v1    hogeuser

REGISTRY が設定されていればOKです。

Build and Deploy

準備ができましたので、FunctionのBuildとDeployを行っていきます。前回と同じくruntimeはgoで行きます。

まずは前回と同じくInitial Functionを作成します。

$ fn init --runtime go sample-docker

func.yaml は以下のとおりです。

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

生成したFunctionのImageを作成し、fn-serverへDeployします。

$ fn deploy --app sample-docker --no-bump
Deploying sample-docker to app: sample-docker
Building image hogeuser/sample-docker:0.0.6 ........
Parts:  [hogeuser sample-docker:0.0.6]
Pushing hogeuser/sample-docker:0.0.6 to docker registry...The push refers to repository [docker.io/hogeuser/sample-docker]
dd51cf4366ae: Pushed
2a92e0de9abf: Pushed
97dedccb7128: Pushed
c9e8b5c053a2: Pushed
0.0.6: digest: sha256:beb34cd4af72d01fxxxxxcfd901f25a8143aaaaaaa9ddb74abf9b9cf6723f838 size: 1156
Updating function sample-docker using image hogeuser/sample-docker:0.0.6...

前回の deploy コマンドとは異なり --local がありません。ImageはDocker Hubの指定したユーザーのRepositoryへとPushされました。

オプションの `--no-bump` はTagバージョンをインクリメントせずにImageの作成を行います。こちらのオプションが存在しない場合は、ImageのTagは `0.0.2` へインクリメントされてDocker HubへPushされます。

念の為Docker HubのRepositoryにPushされているかも確認してみましょう。

これでFnサーバーを新たに立ち上げてもFunctionを再度Deployする必要がなく、ImageをPullするだけで同じFunctionが実行可能です。

Execution

では準備が整いました。Functionを実行してみます。前回はImageのPushをスキップしているだけなので実行結果に大きな違いはありません。

$ fn invoke sample-docker sample-docker
{"message":"Hello World"}

実行引数も問題ありません。

$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker
{"message":"Hello KOMURO"}

サーバー側のログを確認してみると、Containerが立ち上がりFunctionが実行されているのが分かります。

time="2018-10-26T09:00:06Z" level=info msg="starting call" action="server.handleFnInvokeCall)-fm" app_id=01CT94RF05NG8G00GZJ0000001 container_id=01CTQSKG05NG8G00GZJ0000003 fnID=01CT94RF15NG8G00GZJ0000002 fn_id=01CT94RF15NG8G00GZJ0000002 id=01CTQSM97NNG8G00GZJ0000004

Problems

Docker ImageをRepositoryから取得する際にはDocker Pullが必要になります。fnの場合、Serverをスケールする必要がある場合は以下のような動作になるかと思います。

  1. fn-serverのコンテナが新たに立ち上がる
  2. Functionのメタデータやキューイングに接続
  3. Load Balancerに参加
  4. Functionの呼び出しに応じて必要なDocker ImageをPull
  5. ImageからContainerを立ち上げてFunctionを実行

FunctionをDeployする際に、その際に起動しているfn-server側にもImageが配置されているようです。

そこで試しにfn-server内のImageをわざと削除してみて、fn-serverがFunction Imageを持っていない状況でFunctionを呼び出した場合にどうなるかを試してみました。

DeployされたDocker Imageを削除

$ docker exec -it xxxxxx /bin/sh
/app # docker images
REPOSITORY                                 TAG                 IMAGE ID            CREATED              SIZE
hogeuser/sample-docker                       0.0.6               bbbbbbbbbbb        2 minutes ago        15.6MB
<none>                                     <none>              cccccccccccc        2 minutes ago        667MB

/app # docker rmi bbbbbbbbbbb
Untagged: hogeuser/sample-docker:0.0.6
Untagged: hogeuser/sample-docker@sha256:61ed378152dada83145d1d24c76b1a43b530ef92330ee45941dfee90423f2487
Deleted: sha256:1133bda39eea0acde61f2008b2c74080b7712cf2675f7be303725001006448ff
Deleted: sha256:7eaf1b7c4b56a589f8c130e8f7f1477040a9f960bd5d692e2ec57bd8c86a9eda
Deleted: sha256:266680a62b4dbea0e614f0abe2608d534ea83e04a24b208d820f0a014ced43c5
Deleted: sha256:d1f7aad52cda8ac95ceab034d4aea86b50fc4cb49f4e2716a972cc82b14769c9
Deleted: sha256:3d442263cf619f365f1c24094f15b82ab8c3d4840c483a7aafb695a81eae0163

先程と同じく実行してみます。

$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker
{"message":"Failed to pull image 'hogeuser/sample-docker:0.0.6': pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'"}

Fn: Error calling function: status 404

See 'fn <command> --help' for more information. Client version: 0.5.15

エラーが返ってきました。

サーバー側のログを確認

time="2018-10-26T10:41:39Z" level=info msg="Pulling image" app_id=01CT94RF05NG8G00GZJ0000001 cpus= fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username=
time="2018-10-26T10:41:42Z" level=error msg="Failed to pull image" app_id=01CT94RF05NG8G00GZJ0000001 cpus= error="API error (404): pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'" fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username=
time="2018-10-26T10:41:42Z" level=info msg="filtering error" app_id=01CT94RF05NG8G00GZJ0000001 cpus= error="No such container: 01CTQZE7MDNG8G00GZJ000000A" fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128

下記のログを見るに docker login を実行せよとのことです。

error="API error (404): pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'"

しかしfn-serverの起動プロセスが走り始めてしまうとdocker login を実行するタイミングがありません。

fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username=

認証情報がないためRegistryの情報やUsernameの情報が抜けているのも確認できます。これではPullできないのは当然です。うーん、どうすれば良いでしょうか??

docker loginでの認証

再度サーバーのログを確認してみるとこんなログが出ているのが確認できました。

time="2018-10-26T08:51:12Z" level=info msg="no docker auths from config files found (this is fine)" error="open /root/.dockercfg: no such file or directory"

ふむー。.dockercfg ファイルが存在しないというログが少々気になります。 *1しかし、fn-serverのIssueを見てみても問題なさそう

これは関係なさそうなので別の設定が足りていない模様。

Docker versionを確認

Container内部のClient, Serverのバージョンを確認してみます。

/app # docker version
Client:
 Version:   17.12.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    c97c6d6
 Built: Wed Dec 27 20:05:38 2017
 OS/Arch:   linux/amd64

Server:
 Engine:
  Version:  18.06.1-ce
  API version:  1.38 (minimum version 1.12)
  Go version:   go1.10.3
  Git commit:   e68fc7a
  Built:    Tue Aug 21 17:29:02 2018
  OS/Arch:  linux/amd64
  Experimental: true

む。Clientが微妙に古い。自分のローカルマシンを確認してみます。

$ docker version
Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        e68fc7a
 Built:             Tue Aug 21 17:21:31 2018
 OS/Arch:           darwin/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       e68fc7a
  Built:            Tue Aug 21 17:29:02 2018
  OS/Arch:          linux/amd64
  Experimental:     true

微妙に差異がありますね。気になるところではありますがこれもあまり関係なさそう。ふーむ?

.docker/config.json

Dockerではdocker loginした認証情報を config.json に保管するようです。 *2

fn-serverにおけるconfig.jsonは /root/.docker/config.json に配置されます。

fn-server起動後に確認してみると /root/.docker/config.json は作成されていません。試しにContainer内部でDocker Hubへログインしてみると以下のファイルが作成されました。

{
    "auths": {
        "https://index.docker.io/v1/": {
            "auth": "xxxxxxxxxxxx"
        }
    },
    "HttpHeaders": {
        "User-Agent": "Docker-Client/17.12.0-ce (linux)"
    }
}

authはDocker Hubへのログイン情報である username:password をBase64で変換した値が記述されています。

サーバーが立ち上がっていてもこれがないということは、docker login が実行されていないということ。とすると以下の手順が必要なようです。

  1. fn-serverを起動(Containerを作成して起動)
  2. Container内部で docker login を手動で実行(fn-serverと同じプロセスで?)
    1. .docker/config.json が作成される

手動での実行が厳しい。

docker loginのタイミング

Tutorialには docker login を実行することが求められています。しかしどこでという指定がなさそうです。そして考えうるタイミングとして以下の3つが考えられます。

  1. Containerを起動しているホスト側でdocker loginを実行する
  2. 起動したfn-server Containerへ接続し、fn-serverとは別のプロセスでdocker loginを実行する
  3. fn-serverのプロセス内部でdocker loginを実行する

ホスト側でdocker login

こちらを実行してみましたが、そもそもホスト側はmacOS, Container側はLinuxなのであまり意味がなさそう・・・。当然ながらエラーは解消せず。

/bin/shのプロセスでdocker login

以下のコマンドでContainer内部へ /bin/sh を通して新しいプロセスを作成しdocker loginを実行しました。 /root/.docker/config.json が作成されましたがfn-server側のプロセスはこのファイルを認識してくれず。エラーは解消されません。

fn-serverのプロセス内部でdocker login

fn-serverのプロセスの起動の途中で docker loginを入れることは難しそうです。自分には処理をねじ込む方法が思いつきませんでした。断念。

docker loginを回避させる

docker login を手動で実行するのは厳しそうです。そこでホスト側へDocker Hubのログイン情報のconfig.jsonを事前に準備しておき、Container起動時に規定のパスのファイルをホスト側のファイルでマウントする形で回避します。これならばホスト側でCredential情報を持っているため、fn-serverのContainerが立ち上がるたびにログイン作業を実施するという手間がなくなるはずです。

ホスト側のconfig.jsonをMountさせる

fn-serverの実体はDocker Imageから起動されるContainerです。そのため通常のContainerと同様に docker run で起動させることができます。 fn start ではContainer起動時の細かい制御オプションがなさそうなので、 docker run で細かいオプションを自分で設定してfn-serverの起動を試みます。

実行コマンドは以下になります。

$ docker run --rm --privileged -t -i -v /Users/hogeuser/Develop/temp/config.json:/root/.docker/config.json -h fnserver -p 8999:8080 fnproject/fnserver

これで正常にfn-serverの起動ができました。重要そうな部分のオプションのみ下記に解説しておきます。

オプション 説明
--rm ContainerがStopした際に自動的にContainerを削除する設定です。起動オプションを試行する都合上、Containerが終了したらそのまま削除までやってほしいので指定します(別になくてもいい)
--privileged マウント先のパスを見ると分かりますが、root配下のファイルを書き換える形になります。そのため、root権限での作業が必要になります。これを指定しない場合は Permission Denied によりContainerの起動に失敗し、fn-serverのプロセスが立ち上がりません(設定ファイルの配置の都合上必須)
-v /Users/hogeuser/Develop/temp/config.json:/root/.docker/config.json ホスト側のファイルをContainer側の指定されたパスへマウントさせます(設定ファイルを書き換えないと意味がないので必須)
-p 8999:8080 fn-serverのプロセスは8080で立ち上がりますが、前回までのcontextの設定上、8999番ポートを利用してAPIは実行されます。Container内部で8999を8080へルーティングしてもらいます。

上記コマンドでは `-d` のデタッチ指定をしていません。サーバーのログを見て正常に動いているかを確認したかったためです。特に問題がなければ `-d` を指定して起動としたほうが良いかと思います。ちなみにその際は `--rm` は同時に指定できないとのことなのでご注意ください。http://docs.docker.jp/engine/reference/run.html

Invoke

サーバーの準備ができたので、前回までと同じくFunctionの実行をしてみます。

$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker
{"message":"Hello KOMURO"}

正常に実行できました。前回まで出ていたエラーも出ていません。Docker Container内部のImageを見てみるとこの通り。

/app # docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
hogeuser/sample-docker   0.0.6               91b93c79c029        6 minutes ago       15.6MB

正常にPullできているようです。サーバー側のログも確認します。

INFO[2018-10-28T12:06:24Z] Fn serving on `:8080`                         type=full
INFO[2018-10-28T12:11:28Z] Pulling image                                 app_id=01CTX9BXHENG8G00GZJ0000001 cpus= fn_id=01CTX9BXHQNG8G00GZJ0000002 format=http-stream id=01CTX9C3R4NG8G00GZJ0000005 idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry="https://index.docker.io/v1/" stack=PrepareCookie username=hogeuser
INFO[2018-10-28T12:11:38.353638300Z] ignoring event                                module=libcontainerd namespace=moby topic=/containers/create type="*events.ContainerCreate"
INFO[0317] shim docker-containerd-shim started           address="/containerd-shim/moby/372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da/shim.sock" debug=false module="containerd/tasks" pid=173
WARN[2018-10-28T12:11:38.682769200Z] unknown container                             container=372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da module=libcontainerd namespace=plugins.moby
WARN[2018-10-28T12:11:38.704735200Z] unknown container                             container=372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da module=libcontainerd namespace=plugins.moby
INFO[2018-10-28T12:11:38Z] starting call                                 action="server.handleFnInvokeCall)-fm" app_id=01CTX9BXHENG8G00GZJ0000001 container_id=01CTX9C3R4NG8G00GZJ0000005 fnID=01CTX9BXHQNG8G00GZJ0000002 fn_id=01CTX9BXHQNG8G00GZJ0000002 id=01CTX9C3R2NG8G00GZJ0000003

unknown containerという WARN ログが出ているのが若干気になるものの、正常にPullできているようです。今まで表示されていなかった username にも正常にユーザー名があるのが確認できます。

まとめ

Docker Hubを利用してRemote RepositoryへのImageのPush及びPullが行えることを確認しました。Tutorialは一通りうまくいくのですが、docker imageがない場合はどうなるんだろうと思って実行した結果、予想外の結果になったため調査してみました。結局のところ、fnコマンドでの実行ではなくdocker runで直接Containerを自分で立ち上げるという折角細かいオプションをうまくラップして隠蔽している裏側のコマンドを自分で叩くという解決方法になってしまい、これで良いのか?という気持ちです。

一応、回避策は見つけられたものの、fn-serverの動作をまだ完全に正しく理解しているわけではないため、オプションを見逃しているかもしれません。しかし、回避策によって新しくサーバーが立ち上がってImageない状況でもFunctionを呼び出すと自動的にImageをPullしてFunctionが実行されるという意図通りの動作が確認できました。おそらく今後も様々なアップデートで進化していくことでしょう。

まだまだ開発途中であるため、今回の事象はなんとか英語でまとめてIssueなりで報告しようかと。次は多分lbかLocal Repositoryあたりのネタを書こうかなと思います。

knative も気になるため、並行して追っていければ。

それではご機嫌やう。

参照

脚注

  1. http://docs.docker.jp/docker-hub/accounts.html
  2. http://docs.docker.jp/engine/reference/commandline/login.html