florestを使ってWebAPIを作ってみた

この記事は[Go (その3) Advent Calendar 2016](http://qiita.com/advent-calendar/2016/go3)の9日目の記事です。

マイクロサービスの調査のいっかんとして、go製のWebAPI用フレームワーク[florest](https://github.com/jabong/florest-core/wiki/Workflow)というフレームワークを試してみたので、ここにまとめる。

# 前提条件

ここでは以下のバージョンを使用した。

- go 1.7.3
- florest (githubの2016/11/7のmasterのhead）
- [gingko](https://onsi.github.io/ginkgo/)

gingkoのインストールは以下

```bash
$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega
```

# プロジェクトの作成

まず、florest-coreのリポジトリをローカルにcloneします。

```bash
$ git clone https://github.com/jabong/florest-core
```

さらにcloneしたflorest-coreのフォルダに移動して、アプリの雛形をつくります。
`make newapp`を実行する際に環境変数NEWAPPに展開先のフォルダを指定します。

```bash
$ cd florest-core
$ make newapp NEWAPP="~/newapp"
building new app
copying dependent libs
$ cd ~/newapp
```

これでホームフォルダの下にnewappが作られました。

ファイルの構成は以下のような感じです。

```bash
.
├── Makefile
├── _libs
├── config
│   ├── logger
│   │   └── logger.json
│   └── newapp
│       └── conf.json
├── main.go
├── scripts
│   ├── README.md
│   ├── logger.sh
│   └── swagger.sh
└── src
    ├── README.md
    ├── common
    │   ├── appconfig
    │   │   └── application_config.go
    │   └── appconstant
    │       └── error_codes.go
    ├── hello
    │   ├── api_definition.go
    │   ├── data_structures.go
    │   ├── hello_world.go
    │   ├── hello_world_health_checker.go
    │   └── swagger.go
    └── test
        ├── coverage.sh
        ├── servicetest
        │   ├── config_initialize.go
        │   ├── logger_initialize.go
        │   ├── service_initialize.go
        │   ├── service_test.go
        │   ├── service_test_helper.go
        │   ├── test_web_server.go
        │   └── testdata
        │       ├── testconf.json
        │       └── testloggerAsync.json
        └── utils
            ├── request_helper.go
            └── response_status_helper.go
```

さらに実行エラーなどを吐き出すログ用のフォルダを作成します。
デフォルトでは、`/var/log/アプリ名`に作られるので、sudoを使ってフォルダを作成したあとに、florestを実行するユーザーに所有者を変更しておきます。

```bash
$ sudo mkdir /var/log/newapp
$ sudo chown myuser /var/log/newapp
```

最低限の準備ができたので、試しにデプロイします。

```bash
$ cd ~/newapp
$ make deploy
```

`bin`フォルダ配下に実行ファイルが作成されます。binフォルダに移動して実行します。

```bash
$ cd bin
$ ./newapp
APPLICATION BEGIN
2016/11/15 00:14:41 Web server Initialization begin
2016/11/15 00:14:41
Initializing Config
config &{AppName: AppVersion: ServerPort: LogConfFile: MonitorConfig:{APPName: APIKey: APPKey: AgentServer: Platform: Verbose:false Enabled:false MetricsServer:} Performance:{UseCorePercentage:0 GCPercentage:0} DynamicConfig:{Active:false RefreshInterval:0 ConfigKey: CacheKey:} HTTPConfig:{MaxConn:0 MaxIdleConns:0 ResponseHeaderTimeout:0 DisableKeepAlives:false} Profiler:{Enable:false SamplingRate:0} ResponseHeaders:{CacheControl:{ResponseType: NoCache:false NoStore:false MaxAgeInSeconds:0}} ApplicationConfig:0xc4200cbe20 AppRateLimiterConfig:<nil>}
2016/11/15 00:14:41 Application Config Created
2016/11/15 00:14:41
Global Config=&{AppName:newapp AppVersion:1.0.0 ServerPort:8080 LogConfFile:conf/logger.json MonitorConfig:{APPName:newapp APIKey: APPKey: AgentServer:datadog:8125 Platform:DatadogAgent Verbose:false Enabled:false MetricsServer:datadog:8065} Performance:{UseCorePercentage:100 GCPercentage:1000} DynamicConfig:{Active:false RefreshInterval:0 ConfigKey: CacheKey:} HTTPConfig:{MaxConn:0 MaxIdleConns:0 ResponseHeaderTimeout:0 DisableKeepAlives:false} Profiler:{Enable:false SamplingRate:0} ResponseHeaders:{CacheControl:{ResponseType: NoCache:false NoStore:false MaxAgeInSeconds:0}} ApplicationConfig:0xc4200cbe20 AppRateLimiterConfig:<nil>}
2016/11/15 00:14:41
Application Config=&{Hello:{ResponseHeaders:{CacheControl:{ResponseType: NoCache:false NoStore:false MaxAgeInSeconds:0}}}}
No of Cpu Core to be Used = 4
```

8080ポートにアクセスしてみるとサーバが起動していることがわかります。

# エンドポイントの追加

bookという新しいエンドポイントを追加します。

## 必要なファイルを用意
最初に、srcフォルダの下のhelloフォルダをコピーして、ファイルをリネームして、
内部の変数名などの値を全て置換します。

```bash
$ cp -R src/hello src/book
$ find src/book -name "hello_world*" -exec rename s/hello_world/book/ {} +
$ find src/book -name "*.go" -exec sed -i 's/helloWorld/book/g' {} +
$ find src/book -name "*.go" -exec sed -i 's/hello/book/g' {} +
$ find src/book -name "*.go" -exec sed -i 's/HelloWorld/Book/g' {} +
$ find src/book -name "*.go" -exec sed -i 's/Hello/Book/g' {} +
```

## main.goへのAPIの追加
次にmain.goに、さきほど複製してリネームしたbook APIへのimportの追加と、
florestのserviceにBookAPIを追加します。

```go
import (
        "common/appconfig"
        "common/appconstant"
        "fmt"
        "hello"
        "book" // 追加

        "github.com/jabong/florest-core/src/core/service"
)

//main is the entry point of the florest web service
func main() {
        fmt.Println("APPLICATION BEGIN")
        webserver := new(service.Webserver)
        registerConfig()
        registerErrors()
        registerAllApis()
        webserver.Start()
}

func registerAllApis() {
        service.RegisterAPI(new(hello.HelloAPI))
        service.RegisterAPI(new(book.BookAPI)) // 追加
}

```

## エンドポイントのパスの設定

`src/book/api_definition.go`

で、ホスト名以下のパス名を定義します。
さらに、Book IDを指定して、指定のIDの情報が取得できるように、パス内にリクエストパラメータ`bookId`を追加します。

```go
func (a *BookAPI) GetVersion() versionmanager.Version {
        return versionmanager.Version{
                Resource: "BOOK", //エンドポイントの終端のパス名
                Version:  "V1", // アプリ名の次にくるバージョン名
                Action:   "GET", // リクエストメソッド
                BucketID: constants.OrchestratorBucketDefaultValue, //todo - should it be a constant
                Path:     "{bookId}", // bookIdを取得できるようにする
        }
}
```

上記の設定により

`http://ホスト名:8080/newapp/v1/book/{bookId}`

というパスが定義されます。

`newapp`の部分は、`bin/conf/conf.json`の、ルートの`AppName`キーで定義されている値になります。

## リクエストを受けた時の処理の追加

`src/book/book.go`の、`func (a Book) Execute(io workflow.WorkFlowData) (workflow.WorkFlowData, error)`にBook APIの処理ロジックを記載します。

引数の`io`にはリクエストの情報とレスポンスの情報が含まれます。

### リクエストパラメータの取得

`io`の`IOData.Get`メソッドを使って値を取得します。

引数に入る値は、[request_response_constants.go](https://github.com/jabong/florest-core/blob/master/src/common/constants/request_response_constants.go)に定義されている定数を使います。`PathParams`を取得したいので、引数に`constants.PathParams`
を入力します。

```go
        pathp, _ := io.IOData.Get(constants.PathParams)
        fmt.Println(pathp)
```

`deploy`して実行します。サーバの起動を確認したら、コンソールからcurlでbookIdを指定してリクエストを投げてみます。

```bash
$ curl -XGET "http://localhost:8080/newapp/v1/book/123" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   158  100   158    0     0  22829      0 --:--:-- --:--:-- --:--:-- 26333
{
  "status": {
    "httpStatusCode": 200,
    "success": true,
    "errors": null
  },
  "data": null,
  "_metaData": {
    "urlParams": {},
    "apiMetaData": {}
  }
}
```

サーバを起動しているコンソール側のスタンダードアウトには

```bash
123
```

と、リクエストパラメータに指定した値が取得できていることがわかります。

### レスポンスの記述

処理した結果などをレスポンスの内容に記述するには、`io`の`IOData.Set`メソッドを使います。

こちらも先ほどと同じように、第一引数には、[request_response_constants.go](https://github.com/jabong/florest-core/blob/master/src/common/constants/request_response_constants.go)の定数から選択します。

レスポンスのJSONのdataキーの値に結果をレンダリングするには、`constants.Result`を使います。

```go
        io.IOData.Set(constants.Result, "test")
```

同様に、DeployしたあとにWebAPIのサーバを起動して、curlを使ってレスポンス値を確認します。

```bash
$ curl -XGET "http://localhost:8080/newapp/v1/book/123" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   125  100   125    0     0   8965      0 --:--:-- --:--:-- --:--:--  9615
{
  "status": {
    "httpStatusCode": 200,
    "success": true,
    "errors": null
  },
  "data": "test",
  "_metaData": {
    "urlParams": {},
    "apiMetaData": {}
  }
}
```

第2引数に指定した文字列`test`がdataキーに入っていることがわかります。

# Swaggerの出力

## 依存のインストール

Swaggerを扱うために、まずgoのswaggerライブラリをインストールします。

```bash
$ go get github.com/yvasiyarov/swagger
```

## Swaggerの定義を記述

`src/book/swagger.go`

にAPIの仕様を記述します。

`package` の記述の上に、APIのバージョンと`basePath`を記述します。

```go
// @APIVersion 1.0.0
// @basePath /newapp/v1
package book
```

次にtestを取得するメソッドを記述します。

```go
// @Title test
// @Description get test book
// @Accept  json
// @Param   BookId     path    string     true        "book_id"
// @Router /book/{BookId} [get]
func test() {}
```

メソッド名は、`nickname`として出力されます。
そのほかの定義については、

[Whats swagger](https://github.com/swagger-api/swagger-ui#whats-swagger)

を確認してください。

## Swaggerの生成

Swaggerを生成するために、以下のコマンドを実行します。

```bash
$ make deploy SWAGGER=book
```

サーバを起動します。そうすると、以下のURLにアクセスすることで、swagger形式のJSONにアクセスできます。

http://localhost:8080/swagger/v1/book/apidocs.json

ここまでできれば、あとは[Swagger UI](http://swagger.io/swagger-ui/)を使ってドキュメントを生成したりなど、やり放題です。

# その他に提供される機能

ここまで、最低限の機能について、紹介していました。

florestで用意されている他の機能としては、

- MySQL Components
- mongodb Components
- A/B Test
- 非同期のLogging (Logstash形式のJSONに対応)
- Datadogを使ったレスポンスタイムなどのモニタリング
- 各メソッドなどコードの実行時間の計測
- バックグラウンドで非同期実行されるWorker pool
- エラーやタイムアウトなどHttp Utilitiy
- Fault toleranceな機能
- 単純なバリデーション
- Rate limit

などが提供されます。

# まとめ

駆け足でまとめたため、説明不足の部分が多々ありますが、go初心者の自分には、デザインパターンなど学ぶ部分が多々あり勉強になりました。