OpenFaaSを使ってGo言語でFunctionを書いて、AWSに展開したDocker環境にデプロイするまで

OpenFaaSは聞いたことがあるでしょうか。
まだ生まれて半年ほどしか経っていませんが、GithubStar数は6000を超える勢いで成長している有望なフレームワークです。

2017年10月18日に行われたServerless Meetup Osaka #4でOpenFaaSのことを聞いたので早速試してみました。

どこまでやるかというと、
OpenFaaSのAuthorである[Alex Ellisのブログ](https://blog.alexellis.io/serverless-golang-with-openfaas/)で紹介されている、OpenFaaSでGo言語の関数を動かして、さらにクラウドに展開するところまでやってみたいと思います。

今回やってみて、日本語の情報がほぼ皆無で、英語でも情報がかなり少なかったので、丁寧に手順を載せておきます。

# OpenFaaSいいところ

まとめとして先に良かったことを書いておきます。

- Dockerイメージになるなら何でも動かせる
- とはいえ、メジャーな言語のテンプレートを用意している
- 手軽さは既存のFunction as a Serviceと変わらない

スケール・コストなどの観点はチュートリアルでは評価しきれないので、言及しません。

# Faas CLIのインストール

`Docker CE 17.05`以上がインストールされていることが必要です。

コマンドでさくっとインストールできます。

```
curl -sSL https://cli.openfaas.com | sudo sh
```

brewコマンドが使えるなら、

```
brew install faas-cli
```

![Selection_002.png](https://qiita-image-store.s3.amazonaws.com/0/193814/0aabcef2-e307-3285-ccfe-08862c978591.png)

# OpenFaaSデプロイ環境をローカルに追加

Kubernetesに比べると簡単に扱えると感じたDocker Swarmにデプロイします。

まずはDocker Swarm自体の初期化して、managerとworkerを動作させます。

```bash
docker swarm init
```

以下のコマンドでFaaSスタックをデプロイします。

```bash
git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh
```

このデプロイした環境はどこにいるかというと、Docker Service (Swarm Mode) として動いているので、`docker service ls`のコマンドで確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/193814/fc67d9fb-2b0f-9f55-2b82-d6e865a819b0.png)


ローカルのデプロイ環境には`http://localhost:8080`でアクセスできるので開くと、次のようなFunction管理ポータルが表示されます。
Linuxの場合は`http://127.0.0.1:8080`で開く必要があるかもしれません。

![Selection_006.png](https://qiita-image-store.s3.amazonaws.com/0/193814/9085ef61-e0dc-9c78-75f3-a6d38c00f13a.png)


# Go言語のインストール

[こちら](https://golang.org/doc/install)の公式インストールガイドを参考にインストールしてください。

v1.8.3かそれ以降が必要です。
`gvm`でインストールしても問題ありません。

![Selection_003.png](https://qiita-image-store.s3.amazonaws.com/0/193814/01f055a1-1425-3674-daac-b983897b3be7.png)

`$GOPATH`が設定されているか確認しておきます。
![Selection_004.png](https://qiita-image-store.s3.amazonaws.com/0/193814/624f820f-8c0c-fb12-ac3c-d93f8b836202.png)


# OpenFaaSプロジェクトの生成

まずはプロジェクトフォルダーを作成します。
```
mkdir -p $GOPATH/src/functions
cd $GOPATH/src/functions
```

続いてFaaS CLIを使ってプロジェクトテンプレートを生成します。
名前は[参考記事](https://blog.alexellis.io/serverless-golang-with-openfaas/)通り`gohash`です。

```
faas-cli new --lang go gohash
```

![Selection_005.png](https://qiita-image-store.s3.amazonaws.com/0/193814/1d1ffe7a-05e0-0d82-18e5-99f91410b537.png)

# プロジェクトの中身を確認する

`gohash.yml`ファイルにはテンプレートで作成されたFunctionとローカルの実行環境についての設定が書き込まれています。

```yml:gohash.yml
provider:
  name: faas
  gateway: http://localhost:8080

functions:
  gohash:
    lang: go
    handler: ./gohash
    image: gohash
```

`gohash/handler.go`にはHello Worldなコードが書かれています。

```go:gohash/handler.go
package function

import (
	"fmt"
)

// Handle a serverless request
func Handle(req []byte) string {
	return fmt.Sprintf("Hello, Go. You said: %s", string(req))
}

```

そして`template`内には、各言語のテンプレートもありますが、Go言語のものがちゃんとあります。
`main.go`ではOpenFaaSの仕様通り、 **STDIN** を受け取って、対応するFunctionを呼び出した結果を **STDOUT** に送るというシンプルな作りとなっています。

```go:template/go/main.go
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"

	"handler/function"
)

func main() {
	input, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		log.Fatalf("Unable to read standard input: %s", err.Error())
	}

	fmt.Println(function.Handle(input))
}

```

Dockerfileが各言語の環境ごとに用意されていて、ミニマルな実行環境を作成し、開発者が用意したFunctionコードをコンテナ内に格納してから最後にWatchdogを起動するという動きになっているようです。

WatchdogはGo言語で書かれた小さなHttpサーバーです。

```:template/go/Dockerfile
FROM golang:1.8.3-alpine3.6
# ---------- 略 -----------
WORKDIR /go/src/handler
COPY . .
# ---------- 略 -----------
CMD ["./fwatchdog"]
```

# まずはデプロイしてみる

テンプレートを導入した時点で、環境構築が無事に完了しているか確認するために、HelloWorldのままデプロイしてみます。

```bash
faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml
```
BuildはDockerのイメージをダウンロードするところから始まるので、最初の実行は1分ほど待つかもしれません。

とくにエラーが表示されずに、
![Selection_009.png](https://qiita-image-store.s3.amazonaws.com/0/193814/f05d1e02-3dbc-407a-ecc5-f88119748a50.png)
という風な表示がされたら成功です。

`http://localhost:8080`にアクセスして`gohash`が追加されているか確認します。
左のFunction一覧から`gohash`を見つけたら適当なRequest bodyを打ち込んで **INVOKE** します。

![Selection_007.png](https://qiita-image-store.s3.amazonaws.com/0/193814/2ce641cd-389b-f2b6-2b2e-e7fc812dc441.png)

これで無事にデプロイできることが確認できました。


# Functionを実装していく

今回はgo1.9を使ったので、コンテナ内のgoも同じバージョンにしておきます。
今回のコードでは変更しなくても特に問題にはならないと思います。
コンテナ生成Dockerfileを書き換えてしまいます。

```:template/go/Dockerfile変更前
FROM golang:1.8.3-alpine3.6
```

```:template/go/Dockerfile変更後
FROM golang:1.9-alpine3.6
```

go言語のパッケージ管理ツールとして`dep`をインストールします。

```bash
go get -u github.com/golang/dep/cmd/dep
```
そして、Goのデータ(Struct)からハッシュを生成する`structhash`を`dep`を使ってインストールします。
ただし、faas-cliがDockerコンテナをビルドするときに、`template/go/`をワーキングディレクトリとしてビルドを行うため、`dep`の実行はこのディレクトリに移動して行う必要があります。

```bash
cd template/go/
dep init
dep ensure -add github.com/cnf/structhash
```

こうすると、`Gopkg.lock`、`Gopkg.toml`、`vernder/`が`template/go/`以下に生成されます。
ちなみに、`gvm`環境だと`dep ensure`でライブラリがうまくインストールされないことがありますが、goのコードがビルドされるのはコンテナ内なので、一応そのまま進めても大丈夫です。

きちんと開発を進める場合は`gvm linkthis`で`dep`がしっかりと使えるようにします。


それでは、受け取った文字列データからハッシュを生成するコードに変更しましょう。

```go:gohash/handler.go
package function

import (
	"fmt"

	"github.com/cnf/structhash"
)

// S is sample struct
type S struct {
	Str string
	Num int
}

// Handle a serverless request
func Handle(req []byte) string {
	s := S{string(req), len(req)}

	hash, err := structhash.Hash(s, 1)
	if err != nil {
		panic(err)
	}
	return fmt.Sprintf("%s", hash)
}

```

# プロジェクトのデプロイ
では、作成したコードをデプロイしてみましょう。

```bash
faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml
```

ポータルの実行結果はFunctionのデプロイで新しいものに置き換えたので結果が変わっています。

![Selection_010.png](https://qiita-image-store.s3.amazonaws.com/0/193814/672ae80c-efe3-1ff4-31e7-755f39c123b4.png)

また、もちろんAPI URLも用意されているので、直接呼び出すこともできます。

```
curl -X POST -d "てすとめっせーじ" http://localhost:8080/function/gohash
```

![Selection_011.png](https://qiita-image-store.s3.amazonaws.com/0/193814/11d76141-fb97-24ca-f59d-ce270302267c.png)

# テストを追加する

テンプレートのDockerfileには`go test`でテストを実施しているのですが、今のところテスト用のgoファイルは生成されないようです。

今は自分で作りましょう。

```
touch gohash/handler_test.go
```

```go:gohash/handler_test.go
package function

import "testing"

func TestHandleReturnsCorrectResponse(t *testing.T) {  
    expected := "v1_b8dfcbb21f81a35afde754b30e3228cf"
    resp := Handle([]byte("Hello World"))

    if resp != expected {
        t.Fatalf("Expected: %v, Got: %v", expected, resp)
    }
}
```

テストを作ったところで、わざとgohash Functionを間違えて修正してしまいましょう。

```go:handler.go修正前
hash, err := structhash.Hash(s, 1)
```

```go:handler.go修正ミス
hash, err := structhash.Hash(s, 2)
```

この状態で再びビルドを実行します。

```
faas-cli build -f gohash.yml
```

ちゃんとビルド中にテストで失敗してくれます。
![Selection_012.png](https://qiita-image-store.s3.amazonaws.com/0/193814/5781ef0a-0cf1-67e4-256b-fb2544e7b43f.png)

コードを元に戻してビルドが成功することを確認します。

# DockerHubにビルドしたイメージをPUSH

リモート環境でOpenFaaSを動作させるためには、FunctionのDockerイメージをDockerHubまたは別のレジストリに登録しておく必要があります。

まずは、`[DockerHubのユーザ名]/gohash`となるように、`gohash.yml`を書き換えます。

```yml:gohash.yml書換前
    image: gohash
```
```yml:gohash.yml書換後
    image: gcoka/gohash
```
Docker Hubの登録が済んでいれば、 

```
docker login
```
でログインし、

```
faas-cli push -f gohash.yml
```
でビルドしたイメージをPUSHします。

# AWSにデプロイしてみる

AWS EC2コンソールで SSH KeyPairsを作成しておいてください。
ssh-addもお忘れなく。

```
ssh-add ~/.ssh/yourkey.pem
```
AWSにDockerをデプロイするためのテンプレートが用意されているので、ここにアクセスして、
`Deploy Docker Community Edition (CE) for AWS (stable)`をクリックします。
`use your existing VPC`を選択すると、Docker用ネットワークの設定をいろいろやらないといけなくなり、VPC内のネットワーク構築の知識が必要となるようです。

<https://docs.docker.com/docker-for-aws/#docker-community-edition-ce-for-aws>

以下の設定は環境に合わせて変更が必要です。

- SSHキーにKeyPairsで作成したものを指定。

また、このテンプレートでのCloudFormation実行には、以下のCreateRoleが必要です。
正確な一覧は[こちら](https://docs.docker.com/docker-for-aws/iam-permissions/)。

* EC2 instances + Auto Scaling groups
* IAM profiles
* DynamoDB Tables
* SQS Queue
* VPC + subnets and security groups
* ELB
* CloudWatch Log Group

特に設定は変更していません。
デプロイを試すだけなので、Swarm ManagerとWorkerの数は1ずつにしました。

![Selection_029.png](https://qiita-image-store.s3.amazonaws.com/0/193814/35480747-9eb3-523f-c89b-82fd6b513724.png)

CloudFormationのStackデプロイが完了したら、デプロイ結果のOutputsタブから、Swarm Managerのインスタンスへのリンクが参照できるので、開きます。
![Selection_030.png](https://qiita-image-store.s3.amazonaws.com/0/193814/2ffd0aaa-de92-94da-0d20-99fab8c34c57.png)

![Selection_031.png](https://qiita-image-store.s3.amazonaws.com/0/193814/356111dc-4200-2b91-ac48-7c608e2896cd.png)


このインスタンスにSSH接続を行います。
ユーザーは`docker`を指定します。

```
ssh docker@54.159.253.49
```

OpenFaaSのスタックを導入するために、Gitが必要なので、インストールします。

中身はAlpine Linuxなので、`apk`コマンドでパッケージをインストールします。

```:sshコンソール
sudo apk --update add git
```

ローカルでにインストールしたときと同じコマンドですが再掲。

```:sshコンソール
git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh
```

ではOpenFaaSスタックが無事デプロイされているか確認します。

AWSに構築したテンプレートはインターネットからはLoadBalancerを通してアクセスできるので、LoadBalancerのURLを調べます。DNS name: に表示されているものがそれです。

![Selection_034.png](https://qiita-image-store.s3.amazonaws.com/0/193814/817036b9-6751-f244-3cd9-7d29143e47a7.png)

URLがわかったら、ポート8080を指定してアクセスします。

![Selection_035.png](https://qiita-image-store.s3.amazonaws.com/0/193814/e7ee78b1-99cc-a994-7588-6709f7589840.png)

うまくいっていることを確認したらAWSにデプロイします。
`--gateway`オプションを使えば`gohash.yml`ファイルを書き換える必要がありません。

```
faas-cli build -f gohash.yml --gateway http://your.amazon.aws.loadbalancer.url:8080
```

![Selection_038.png](https://qiita-image-store.s3.amazonaws.com/0/193814/648496d9-44b3-2f86-e874-ac8212bc6e80.png)

![Selection_039.png](https://qiita-image-store.s3.amazonaws.com/0/193814/0bf207b7-9a69-4568-a4fe-4ac50a138f1f.png)

無事にAWSで動きました。

# トラブルシューティング（詰まったところ)

## `faas-cli build`してもコード変更が反映されない

`--no-cache`をつけることで、すべてのビルドをやり直してくれます。

```
faas-cli build -f gohash.yml --no-cache
```

## structhashパッケージがvenderディレクトリにコピーされない

$GOPATHが正しく設定されているか確認してください。

`gvm`を使ってgoをインストールしている場合は、`$GOPATH/src/functions`で`gvm linkthis`を実行すると解決するかもしれません。


## AWS CloudFormationのデプロイに失敗する

`use your existing VPC`のテンプレートを使っている場合は、使わないでVPCの作成もDockerテンプレートに任せてください。

## AWSにデプロイしたFunctionを実行しても500: Internal Server Errorになる

Docker HubにDockerイメージをPUSHしたか確認してください。

## ちゃんと動いているかわからない・・・

docker serviceとして動いているのでdockerコマンドからいくつか情報を得ることができます。

```:指定したFunctionのプロセス動作情報を表示
docker service ps gohash
```

```:指定したFunctionのログを表示
docker service logs gohash
```

```:gatewayのログを表示
docker service logs func_gateway
```

# 最後に
Docker SwarmはMicrosoft Azure Container Serviceもサポートしているのですが、完全なマネージドの場合OpenFaaSが必要とするDockerバージョンが足りていないようです。

今回はDocker Swarmに対してデプロイしましたが、Kubernetesもサポートしているので、そちらも試してみたいと思います。
Kubernetesの場合は、Swarmよりもホスティング対応しているクラウドがいくつかあるようなので、期待が持てますね。

情報がとても少ないので、どんどん試してどんどん情報公開していきましょう！