2018 年振り返り

仕事

サーバサイドでは引き続き Go を書いていました。フロントエンドでは React を書く機会があったのが良かったです。

OSS

今年もコツコツ goa に機能追加したりしていました。

tchssk.hatenablog.com

tchssk.hatenablog.com

multipart/form-data でファイルアップロードする機能は仕事でも使ったのですが便利ですね。

あとは goagen をデフォルト GOPATH や、 Go 1.11 Modules で動くようにしたりしました。

github.com

github.com

v2 についてはひたすらテストコードを書くということをやっていて、数えてみたら Pull request を 36 本出してました。

github.com

これでもテストがない箇所はまだまだあるので根気よく続けていきたいと思っています。

買い物

Amazon Kindle Paperwhite

以前から欲しいなーと思いつつ購入していなかったのですが、サイバーマンデーで結構安くなっていたので書いました。文庫本サイズですが技術書もスマートフォンより読み易くていいですね。

The Last of Us Remastered

今年はたくさんゲームをやったのですがとにかく The Last of Us が最高でした。続編が製作中ということなのでとても期待しています。

iRobot Roomba

アプリや Wi-Fi に対応していない一番廉価なモデルですが掃除自体は問題なくこなしてくれます。障害物にぶつかる時は一応バンパーでショックを軽減する仕組みにはなっているのですが、家具に傷が付くと嫌なのでショックノンテープを貼っています。

Dyson Cyclone V10 Fluffy

こちらは細かい所の掃除用に。サイクロン式はゴミ捨てが面倒な場合が多いと聞きますが、これはワンタッチでゴミが取れるようになっているので便利です。

SHARP ドラム式洗濯乾燥機

乾燥機はヒートポンプ式が良いと聞いたのでデザインが気に入ったこのモデルを買いました。以前は乾燥機の付いていない洗濯機を使っていたので手で干していたのですが、一度乾燥機付きを体験してしまうと戻れませんね。

Panasonic 6ドア冷蔵庫

最初はミラータイプのものを検討していたのですが、結果的にはインテリアに良く馴染んでくれたのでブラウンを選んで正解でした。

Sony 55V型 有機EL テレビ

画面自体が振動して音が出る、という謎技術のテレビなのですが結構音も良いしベゼルもスリムで大変気に入っています。

I-O DATA HDMI キャプチャーボード

スプラトゥーンで自分のプレイを録画して見返す用に購入しました。比較的安価ですが 1080/60p で録画できて良いです。

一生もの。

f:id:tchssk:20181231213941j:plain

総括

新居が最高過ぎて下半期はほとんど家にいたような気がします。来年はもう少し運動しなければ ...

Go 1.11 Modules でパッケージ管理を行う

この記事は Go4 Advent Calendar 2018 の 22 日目の記事です。

今年の 8 月に Go 1.11 がリリースされました。このバージョンでは Modules と呼ばれる新しいコンセプトが導入されています。これはバージョニングとパッケージ配布の仕組みで、 GOPATH 配下以外でのプロジェクト管理を可能にするものです。

これは Go 1.11 の時点では実験的な機能として提供されていて、環境変数 GO111MODULE の設定やプロジェクトが GOPATH 配下にあるかどうかに応じて有効化されるようになっています。このバージョンでフィードバックを取り込みながら Go 1.12 で正式な機能としてリリースされる予定ということです。今回は現時点での Go Modules を試してみたいと思います。

Go 1.11 Modules で依存解決を行う

私は Web API の作成に goa を良く使っているので、その example である goa-cellar に Go 1.11 Modules を適用できるか試してみたいと思います。

1. GOPATH 外にプロジェクトを用意してそのルートに移動する

GOPATH~/go なので、その配下ではない ~/goa-cellargit clone してプロジェクトルートに移動します。

$ git clone git@github.com:goadesign/goa-cellar.git ~/goa-cellar
$ cd ~/goa-cellar

2. Module 定義を初期化して go.mod ファイルを作成する

go コマンドには既にサブコマンドとして mod が組み込まれているのでそれを使って go.mod を作成します。

$ go mod init
go: creating new go.mod: module github.com/goadesign/goa-cellar

3. Module をビルドする

Module をビルドします。 Go Modules ではビルドやテストのタイミングで必要な依存解決が行われます。

$ go build -o cellar
go: finding github.com/goadesign/goa v1.4.0
go: finding github.com/satori/go.uuid v1.2.0
go: finding github.com/go-kit/kit v0.8.0
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/go-logfmt/logfmt v0.4.0
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/hashicorp/go-uuid v1.0.0
go: finding github.com/hashicorp/golang-lru v0.5.0
go: downloading github.com/goadesign/goa v1.4.0
go: downloading github.com/go-kit/kit v0.8.0
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/satori/go.uuid v1.2.0
go: downloading github.com/go-logfmt/logfmt v0.4.0
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
# github.com/goadesign/goa/uuid
../go/pkg/mod/github.com/goadesign/goa@v1.4.0/uuid/uuid.go:18:23: not enough arguments in call to uuid.Must
        have (uuid.UUID)
        want (uuid.UUID, error)

おっと、エラーが出てしまいました。コードを見てみます。

https://github.com/goadesign/goa/blob/v1.4.0/uuid/uuid.go#L18

import "github.com/satori/go.uuid"

// FromString Wrapper around the real FromString
func FromString(input string) (UUID, error) {
    u, err := uuid.FromString(input)
    return UUID(u), err
}

// NewV4 Wrapper over the real NewV4 method
func NewV4() UUID {
    return UUID(uuid.Must(uuid.NewV4()))
}

これは https://github.com/satori/go.uuid の問題ですね。当初 uuid.Must()uuid.UUID だけを返す関数だったのですが、あるとき破壊的変更が入って error も返す形にシグネチャが変更されました。しかしその変更のあとに新たなバージョンのリリースは行われておらず master ブランチだけに変更が入っている状態です。

goa はこの master ブランチのコードを参照しているのですが、 Go Modules はデフォルトだとタグ付けされた最新のリリースバージョンを選択するため master ではなく v1.2.0 が使われることになります。これがエラーの原因でした。

$ cat go.mod | grep github.com/satori/go.uuid
        github.com/satori/go.uuid v1.2.0 // indirect

このデフォルトのバージョン選択は @version 接尾辞を用いた go get で変更することが出来るので、明示的に master を参照するよう依存を変更します。

$ go get github.com/satori/go.uuid@master
go: finding github.com/satori/go.uuid master
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

再度ビルドしてみます。

$ go build -o cellar
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

今度は成功です。ではサーバを起動してリクエストを送信してみます。

$ ./cellar &
[1] 27478
lvl=info msg=mount ctrl=Account action=Create route="POST /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Delete route="DELETE /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=List route="GET /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Show route="GET /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=Update route="PUT /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Bottle action=Create route="POST /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Delete route="DELETE /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=List route="GET /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Rate route="PUT /cellar/accounts/:accountID/bottles/:bottleID/actions/rate"
lvl=info msg=mount ctrl=Bottle action=Show route="GET /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Update route="PATCH /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Watch route="GET /cellar/accounts/:accountID/bottles/:bottleID/watch"
lvl=info msg=mount ctrl=Public files=public/html/index.html route="GET /ui"
lvl=info msg=mount ctrl=Js files=public/js route="GET /js/*filepath"
lvl=info msg=mount ctrl=Js files=public/js/index.html route="GET /js/"
lvl=info msg=mount ctrl=Swagger files=public/swagger/swagger.json route="GET /swagger.json"
lvl=info msg=listen transport=http addr=:8081
$ http :8081/cellar/accounts
req_id=pTlK94BRv4-1 lvl=info msg=started GET=/cellar/accounts from=::1 ctrl=Account action=list
req_id=pTlK94BRv4-1 lvl=info msg=headers Accept=*/* Accept-Encoding="gzip, deflate" Connection=keep-alive User-Agent=HTTPie/1.0.2                                                                                                      
req_id=pTlK94BRv4-1 lvl=info msg=completed status=200 bytes=114 time=203.016µs ctrl=Account action=list
HTTP/1.1 200 OK
Content-Length: 114
Content-Type: application/vnd.account+json; type=collection
Date: Tue, 18 Dec 2018 13:58:15 GMT

[
    {
        "href": "/cellar/accounts/1",
        "id": 1,
        "name": "account 1"
    },
    {
        "href": "/cellar/accounts/2",
        "id": 2,
        "name": "account 2"
    }
]

$ kill 27478
[1]+  Terminated: 15          ./cellar

問題なさそうです。

依存としてのツール

goa はプロジェクトを構成するコードの大部分を自動生成する仕組みになっており、そのために goagen という専用のツールを用意しています。 Go Modules は、そういったプロジェクトに必要となるツールの管理も出来るようになっています。 Go modules by example に詳しい手順が載っているので、これを参考に goagen を管理してみます。

GOBIN にツールのインストール先を設定する

Go はデフォルトだと go install で作られたバイナリを $GOPATH/bin に配置しますが、 $GOBIN を設定することで対象のパスを変更することが出来ます。今回はツールもプロジェクトのディレクトリ配下に置くようにするため $PWD/bin に設定します。

$ export GOBIN=$PWD/bin
$ export PATH=$GOBIN:$PATH

ツールを import するためのファイルを作成する

ツールの package を import するためのファイルを作成します。先頭行のコメントは Build Constraints と呼ばれるもので、デフォルトのビルド対象から除外することが出来ます。

$ cat tools.go
// +build tools

package tools

import (
   _ "github.com/goadesign/goa/goagen"
)

ツールをインストールする

ではツールをインストールしてみます。

$ go get github.com/goadesign/goa/goagen@master
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa master
go: finding github.com/manveru/faker latest
go: finding github.com/zach-klippenstein/goregen latest
go: finding github.com/dimfeld/httppath latest
go: finding github.com/spf13/pflag v1.0.3
go: finding github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast/astutil latest
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast latest
go: finding golang.org/x/tools/go latest
go: finding golang.org/x/tools latest
go: downloading golang.org/x/tools v0.0.0-20181218204010-d4971274fe38
go: downloading gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
$ tree ./bin/
./bin/
└── goagen

0 directories, 1 file

$PWD/bin/goagen が作成されました。

インストールしたツールを使う

このプロジェクトには Makefile が含まれており、 make することで以下のタスクが実行されるようになっています。

  • 関連する package の go get
  • goagen によって前回生成されたコードの削除
  • goagen によるコードの再生成
  • go build

go get 対象の goa のパッケージに @master を付与しておきます。

diff --git a/Makefile b/Makefile
index 9790e3a..80a82d7 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,9 @@
 # - all is the default target, it runs all the targets in the order above.
 #
 DEPEND=    bitbucket.org/pkg/inflect \
-  github.com/goadesign/goa \
-  github.com/goadesign/goa/goagen \
-  github.com/goadesign/goa/logging/logrus \
+   github.com/goadesign/goa@master \
+   github.com/goadesign/goa/goagen@master \
+   github.com/goadesign/goa/logging/logrus@master \
    github.com/sirupsen/logrus \
    gopkg.in/yaml.v2 \
    golang.org/x/tools/cmd/goimports

make します。

$ make
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa/logging/logrus master
go: finding github.com/goadesign/goa master
go: finding github.com/goadesign/goa/logging master
go: finding bitbucket.org/pkg/inflect latest
go: finding golang.org/x/tools/cmd/goimports latest
go: finding golang.org/x/tools/cmd latest
go: finding golang.org/x/tools latest
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/account_testing.go
app/test/bottle_testing.go
app/test/health_testing.go
app/test/js_testing.go
app/test/public_testing.go
app/test/swagger_testing.go
public/swagger
public/swagger/swagger.json
public/swagger/swagger.yaml
public/schema
public/schema/schema.json
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/account.go
client/bottle.go
client/health.go
client/js.go
client/public.go
client/swagger.go
client/user_types.go
client/media_types.go
public/js
public/js/client.js
public/js/axios.min.js
public/js/index.html
public/js/example.go

無事にビルドできました。

余談ですが、 goagen は今まで GOPATH を前提とした実装になっていたので Go Modules では動きませんでした。実は今回の記事を書くにあたって Go Modules をサポートする Pull request を出したので goa をお使いの方は試してみてもらえると嬉しいです。 (記事の公開前にマージされて良かった😌)

github.com

依存解決でダウンロードされたパッケージ

実は今回の作業を始める前に $GOPATH/src を空にしておいたので、念のため中身を確認してみます。

$ tree ~/go/src/
/Users/tchssk/go/src/

0 directories, 0 files

何もないですね。

Go Modules は依存解決でダウンロードしたパッケージを $GOPATH/pkg/mod/ の配下にキャッシュするようです。

$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/
├── bitbucket.org
│   └── pkg
├── cache
│   ├── download
│   └── vcs
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── hashicorp
│   ├── manveru
│   ├── satori
│   ├── sirupsen
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
└── gopkg.in
    └── yaml.v2@v2.2.2

21 directories, 0 files

このキャッシュは go clean -modcache で削除することが出来ます。

$ go clean -modcache
$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/ [error opening dir]

0 directories, 0 files

また、依存パッケージは go mod vendor でプロジェクト内にコピーすることもできます。

$ go mod vendor                
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/spf13/pflag v1.0.3          
go: finding github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: finding github.com/sirupsen/logrus v1.2.0      
go: finding github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: finding golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/go-logfmt/logfmt v0.4.0        
go: finding github.com/pmezard/go-difflib v1.0.0      
go: finding github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: finding golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: finding github.com/davecgh/go-spew v1.1.1         
go: finding github.com/stretchr/testify v1.2.2       
go: finding github.com/hashicorp/go-uuid v1.0.0     
go: finding github.com/konsorten/go-windows-terminal-sequences v1.0.1
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/stretchr/objx v0.1.1
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/go-kit/kit v0.8.0                  
go: finding github.com/spf13/cobra v0.0.3                 
go: finding github.com/hashicorp/golang-lru v0.5.0               
go: finding golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: finding bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47
go: finding gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go: downloading github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/spf13/cobra v0.0.3                          
go: downloading github.com/go-kit/kit v0.8.0                  
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: downloading gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: downloading golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
go: finding github.com/inconshreveable/mousetrap v1.0.0       
go: downloading github.com/go-logfmt/logfmt v0.4.0       
go: downloading github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding google.golang.org/appengine v1.3.0               
go: downloading github.com/inconshreveable/mousetrap v1.0.0   
go: downloading google.golang.org/appengine v1.3.0
go: finding golang.org/x/net v0.0.0-20180724234803-3673e40ba225  
go: finding github.com/golang/protobuf v1.2.0
go: finding golang.org/x/text v0.3.0    
go: downloading github.com/golang/protobuf v1.2.0
$ tree -L 2 ./vendor/
./vendor/
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── golang
│   ├── hashicorp
│   ├── inconshreveable
│   ├── kr
│   ├── manveru
│   ├── satori
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
├── google.golang.org
│   └── appengine
├── gopkg.in
│   └── yaml.v2
└── modules.txt

20 directories, 1 file

Go Modules の今後

この記事を書いていたらちょうど The Go Blog で Go Modules の記事が公開されていました。

Go Modules in 2019 - The Go Blog

Go 1.12, scheduled for February 2019, will refine module support but still leave it in auto mode by default.

Our aim is for Go 1.13, scheduled for August 2019, to enable module mode by default (that is, to change the default from auto to on) and deprecate GOPATH mode.

2019 年 2 月予定の Go 1.12 は現在と同じようにデフォルトで auto モードで動作するようですね。 Module モードをデフォルトにするのは 2019 年 8 月予定の Go 1.13 からの予定で、そのときには GOPATH モードを非推奨にするということです。楽しみですね。

goa が返すエラーレスポンスの Content-Type を変更する

Web API がエラーレスポンスを返す際にはレスポンスボディでエラーの詳細を示すのが一般的です。 goa には goa.ErrorResponse という型が組み込まれており、 controller に到達する前の処理でエラーが発生した際のレスポンスに使われています。また controller の中で goa.ErrorResponse を使うこともできます。

controller 到達前のエラーレスポンス

例えば /foo というエンドポイントが存在しないのに GET /foo というリクエストが来たときには以下の形のレスポンスが返ります。

{
  "id": "XXXXXXXX",
  "code": "not_found",
  "status" :404,
  "detail": "/foo"
}

この際、レスポンスヘッダの Content-Typeapplication/vnd.goa.error となります。この値は goa.ErrorMediaIdentifier として定義されているため変更することが可能です。例えば Content-Typeapplication/vnd.myservice.error に変える場合は以下のコードを main パッケージに追加します。

package main

import (
    "github.com/goadesign/goa"
)

func init() {
    // Change the media type identifier used for error responses.
    goa.ErrorMediaIdentifier = "application/vnd.myservice.error"
}

controller が返すエラーレスポンス

goa.ErrorResponse には、対応する design 定義として design.ErrorMedia が用意されています。これを design の中で使うことで、あるレスポンスが goa.ErrorResponse のフォーマットであることを明示できます。以下の例では BadRequestErrorMedia を指定することで、不正なリクエストが来たときのエラーレスポンスが goa.ErrorResponse のフォーマットであることを表しています。

Response(BadRequest, ErrorMedia)

controller の実装では ctx.BadRequest() を呼び出すことで Bad Request を返却することができます。その際、引数に *goa.ErrorResponse を渡すとそれがレスポンスボディに設定されます。

ctx.BadRequest(&goa.ErrorResponse{
    Detail: "this is a error message",
})

こちらの Content-Typeapplication/vnd.goa.error ですが、controller 到達前のエラーレスポンスとは違い design.ErrorMedia.Identifier が参照されています。このフィールドには design.ErrorMediaIdentifier が設定されているため、まず design.ErrorMediaIdentifier を変更した上で design.ErrorMedia.Identifier に再設定しておくことで実際の Content-Type を変更することができます。

package design

import (
    "github.com/goadesign/goa/design"
)

func init() {
    // Change the media type identifier used for error responses.
    design.ErrorMediaIdentifier = "application/vnd.myservice.error"
    design.ErrorMedia.Identifier = design.ErrorMediaIdentifier
}

Swagger UI

goa は Swagger 定義を出力できるので Swagger UI と組み合わせて使われることも多いと思います。しかし application/vnd.goa.error のようにサフィックスが付いていない形式だと Try it out ボタンでリクエストを試すときにエラーレスポンスが Unknown response type となり表示されません。

f:id:tchssk:20180922113030p:plain

これを表示させるには、プロジェクトに以下のコードを追加して Content-Typeサフィックス付きの application/vnd.goa.error+json に変更します。

package main

import (
    "github.com/goadesign/goa"
)

func init() {
    // Change the media type identifier used for error responses.
    goa.ErrorMediaIdentifier = goa.ErrorMediaIdentifier + "+json"
}
package design

import (
    "github.com/goadesign/goa/design"
)

func init() {
    // Change the media type identifier used for error responses.
    design.ErrorMediaIdentifier = design.ErrorMediaIdentifier + "+json"
    design.ErrorMedia.Identifier = design.ErrorMediaIdentifier
}

これで Swagger UI でもレスポンスが表示されるようになります。

f:id:tchssk:20180922113527p:plain

goa で multipart/form-data を扱う (ファイルアップロード編)

前回 goa で multipart/form-data を扱う方法についてご紹介しました。

tchssk.hatenablog.com

multipart/form-data に関連してよく取り上げられるのがファイルアップロードですね。 HTML フォームを介したサーバへのファイル送信は多くのウェブサービスで利用されています。しかし前回の記事ではその話については言及しませんでした。理由は「ファイル」を表すデータ型がまだ goa に存在しなかったからです。

goa では Type DSL を使ってユーザ型を定義します。また、ユーザ型の各要素を定義するには Attribute DSL を使用します。

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Required("name", "birthday")
})

Attribute の 2 番目の引数がデータ型です。上記StringDateTime を含む、以下 7 種類が用意されています

  • Boolean
  • Integer
  • Number
  • String
  • DateTime
  • UUID
  • Any

そして今回、新たに 8 種類目の File 型が追加されました。

github.com

では先ほどの design に File 型の要素を追加してみましょう。

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Attribute("icon", File, "Icon")
    Required("name", "birthday", "icon")
})

ここから生成される Go の構造体は以下の形になります。

// ProfilePayload user type.
type ProfilePayload struct {
    // Birthday
    Birthday time.Time `form:"birthday" json:"birthday" xml:"birthday"`
    // Icon
    Icon *multipart.FileHeader `form:"icon,omitempty" json:"icon,omitempty" xml:"icon,omitempty"`
    // Name
    Name string `form:"name" json:"name" xml:"name"`
}

Icon の型が *multipart.FileHeader になっていますね。これは標準パッケージの mime/multipart で提供されている構造体で、ファイル名、 MIME ヘッダ、ファイルサイズを含んでいます。

type FileHeader struct {
        Filename string
        Header   textproto.MIMEHeader
        Size     int64
}

また、この構造体には Open() というメソッドが用意されており、ファイルを multipart.File として取得することが可能です。 multipart.Fileio.Reader などを実装しているので通常のファイルと同じようにように操作することができます。

以上を踏まえて、アップロードされたファイルをサーバのローカルディレクトリに保存する処理を controller に実装してみましょう。

func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error {
    res := &app.ResultMedia{
        Name:     ctx.Payload.Name,
        Birthday: ctx.Payload.Birthday,
    }
    file, err := ctx.Payload.Icon.Open()
    if err != nil {
        return err
    }
    defer file.Close()
    f, err := os.OpenFile("./icons/"+ctx.Payload.Icon.Filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return fmt.Errorf("failed to save file: %s", err) // causes a 500 response
    }
    defer f.Close()
    io.Copy(f, file)
    return ctx.OK(res)
}

前述の通り ctx.Payload.Icon*multipart.FileHeader となっているので Open()multipart.File を取得しています。あとはアップロードされたものと同じ名前のファイルを os.OpenFile() で作成し io.Copy() で書き込むだけです。

この例は既に公式サンプルにも収録されているので以下からご覧ください。

github.com

ファイルアップロードにも対応した goa が益々便利ですね!

goa で multipart/form-data を扱う

multipart/form-data という MIME タイプがあります。これは主に HTML フォームの内容をサーバに対して POST する際に用いられます。

goa の Request Context には http.Request が埋まっているので、そこから MultipartReader()ParseMultipartForm() を呼び出すことで multipart/form-data を扱うことができます。以下は goa の公式サンプル収録されているコードです。

func (c *ImageController) Upload(ctx *app.UploadImageContext) error {
    // Assumes the image is under multipart section named "file"
    reader, err := ctx.MultipartReader()
    if err != nil {
        return goa.ErrBadRequest("failed to load multipart request: %s", err)
    }
    if reader == nil {
        return goa.ErrBadRequest("not a multipart request")
    }
    var images []*app.ImageMedia
    for {
        p, err := reader.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return goa.ErrBadRequest("failed to load part: %s", err)
        }
        f, err := os.OpenFile("./images/"+p.FileName(), os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
            return fmt.Errorf("failed to save file: %s", err) // causes a 500 response
        }
        defer f.Close()
        io.Copy(f, p)
        data := c.saveImage(p.FileName())
        images = append(images, &app.ImageMedia{ID: data.ID, Filename: data.Filename, UploadedAt: data.UploadedAt})
    }
    return ctx.OK(images)
}

multipart.Reader を介して、各 part の内容を取り出しています。

このように、従来から goa では multipart/form-data を扱うことができたのですが、これはあくまでサーバ実装の話です。 design (設計) の中で multipart/form-data であることを明示するための DSL は用意されていなかったので、 Swagger にも formData として定義を出力することはできませんでした。

しかし今回、この機能に関する MultipartForm という新しい DSL が追加になりました。今後は design レベルで multipart/form-data を取り扱うことができます。

github.com

MultipartForm の使用例について、まずは design から見てみましょう。

var _ = Resource("profiles", func() {
    Action("submit", func() {
        Routing(POST("profiles"))
        Payload(ProfilePayload)
        MultipartForm()
        Description("Post accepts a multipart form encoded request")
        Response(OK, ResultMedia)
    })
})

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Required("name", "birthday")
})

MultipartFormAction DSL の中で使用します。これによって goa は当該 Action の Payload を multipart/form-data として扱うようになります。

これを元に出力される Swagger 定義は以下のようになります。

  /profiles:
    post:
      description: Post accepts a multipart form encoded request
      operationId: profiles#submit
      parameters:
      - description: Birthday
        in: formData
        name: birthday
        required: true
        type: string
      - description: Name
        in: formData
        name: name
        required: true
        type: string
      produces:
      - application/vnd.goa.example.form
      responses:
        "200":
          description: OK
          schema:
            $ref: '#/definitions/ResultMedia'
      schemes:
      - http
      summary: submit profiles
      tags:
      - profiles

parameters に書かれている各パラメータが in: formData になっています。これは OpenAPI Specification Version 2 で規定されている form-data を表す定義になります。

最後に controllers の実装です。冒頭に記述した通り、従来は multipart をパースして各 part をひとつずつ取り出す必要がありました。今回の機能追加後、コードは以下の形になります。

func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error {
    res := &app.ResultMedia{
        Name:     ctx.Payload.Name,
        Birthday: ctx.Payload.Birthday,
    }
    return ctx.OK(res)
}

これだけです。 multipart のパースは、このコードに到達する前に goa が自動で行います。各 part の内容は既に ctx.Payload のフィールドにセットされているので、それを使ったコードを書くだけで良いのです。

より詳細な例は以下をご覧ください。

github.com

2017 年振り返り

OSS

昨年に引き続き goa にコミットしていました。 2018 年は v2 が形になるといいですね。

tchssk.hatenablog.com

勉強会

goa 勉強会で発表もしました。

tchssk.hatenablog.com

転職

2 年半勤めた前職を辞めて 11 月から Pacific Porter で働いています。引き続き仕事でも Go を (goa も) 書く毎日です。前職では新規開発に関わることが多かったので、既に動いているシステムに関わるのは久しぶりです。だんだん仕組みがわかっていくのが楽しいですね。

総括

10 月あたりから解けるように時間が過ぎていました。どう考えてもスプラトゥーンのせいです。本当にありがとうございました。

Go で書かれた API Gateway "KrakenD"

この記事は Go3 Advent Calendar 2017 の 10 日目の記事です。

小さな役割を持つサービス群でアプリケーションを構成する、いわゆる Microservices が流行して久しいですね。サービスが細かく分割されるとき、それらを取りまとめるサービスが必要になることがあります。一般に API GatewayAPI Aggregator と呼ばれるものです。

そんな API Gateway のひとつに Go で書かれた KrakenD というものがあります。

f:id:tchssk:20171206184251p:plain KrakenD: API Gateway and Manager

今回はこれを試してみます。

仕様

複数のバックエンド API を集約 (aggregate) する動きを見るため、以下のような仕様でサービスを構築してみます。

  • バックエンドに GitHub と Qiita の Web API を使用する
  • KrakenD に /users/:id というエンドポイントを定義する
  • そこにリクエストが来たらバックエンドの GitHub と Qiita の API にリクエストする
    • GitHubGET https://api.github.com/users/:id
    • Qiita は GET https://qiita.com/api/v2/users/:id
  • それぞれからのレスポンスを "github""qiita" というフィールドに入れてクライアントにレスポンスする

シーケンス図にすると以下のような感じですね。

client     krakend    github      qiita
   |          |          |          |
   +--------->|          |          |    GET /users/:id
   | request  |          |          |
   |          |          |          |
   |          +--------->|          |    GET https://api.github.com/users/:id
   |          | request  |          |
   |          |          |          |
   |          +----------^--------->|    GET https://qiita.com/api/v2/users/:id
   |          |          | request  |
   |          |          |          |
   |          |<---------+          |
   |          | response |          |
   |          |          |          |
   |          |<---------^----------+
   |          |          | response |
   |          |          |          |
   |<---------+          |          |
   | response |          |          |
   |          |          |          |

KrakenD の設定ファイル

まず上記の仕様を KrakenD の設定ファイル (JSON) として用意する必要があります。なんと設定ファイルを作るための Web エディタが用意されているのでそれを使わせてもらいましょう。

KrakenDesigner - KrakenD: API Gateway and Manager

見た目はこんな感じ。

f:id:tchssk:20171206174522p:plain

UI もなかなか悪くなく、サクッと設定ファイルを作ることができました。

{
  "version": 1,
  "default_max_rate": 0,
  "client_max_rate": 0,
  "throttling_header": "",
  "timeout": "3000ms",
  "cache_ttl": "300s",
  "name": "My service",
  "host": [
    "https://api.github.com",
    "https://qiita.com"
  ],
  "endpoints": [
    {
      "endpoint": "/users/{id}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/api/v2/users/{id}",
          "encoding": "json",
          "host": [
            "https://qiita.com"
          ],
          "group": "qiita"
        },
        {
          "url_pattern": "/users/{id}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ],
          "group": "github"
        }
      ]
    }
  ]
}

KrakenD サービスの立ち上げ

最近のソフトウェアらしく Docker Hub でイメージが提供されているので、それを試してみましょう。

$ docker pull devopsfaith/krakend
$ docker run -p 8080:8080 -v $PWD:/etc/krakend/ devopsfaith/krakend

簡単ですね。ではリクエストを送ってみます。 HTTP クライアントは HTTPie です。

$ http localhost:8000/users/tchssk

{
    "github": {
        "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4",
        "bio": null,
        "blog": "https://tchssk.github.io",
        "company": null,
        "created_at": "2015-05-05T15:43:31Z",
        "email": null,
        "events_url": "https://api.github.com/users/tchssk/events{/privacy}",
        "followers": 6,
        "followers_url": "https://api.github.com/users/tchssk/followers",
        "following": 3,
        "following_url": "https://api.github.com/users/tchssk/following{/other_user}",
        "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}",
        "gravatar_id": "",
        "hireable": null,
        "html_url": "https://github.com/tchssk",
        "id": 12257128,
        "location": "Tokyo, Japan",
        "login": "tchssk",
        "name": "Taichi Sasaki",
        "organizations_url": "https://api.github.com/users/tchssk/orgs",
        "public_gists": 0,
        "public_repos": 33,
        "received_events_url": "https://api.github.com/users/tchssk/received_events",
        "repos_url": "https://api.github.com/users/tchssk/repos",
        "site_admin": false,
        "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions",
        "type": "User",
        "updated_at": "2017-12-05T10:08:00Z",
        "url": "https://api.github.com/users/tchssk"
    },
    "qiita": {
        "description": "A web developer who loves Ginger Ale !!",
        "facebook_id": "",
        "followees_count": 0,
        "followers_count": 2,
        "github_login_name": "tchssk",
        "id": "tchssk",
        "items_count": 11,
        "linkedin_id": "",
        "location": "Tokyo, Japan",
        "name": "Taichi Sasaki",
        "organization": "",
        "permanent_id": 83093,
        "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/83093/profile-images/1473702659",
        "twitter_screen_name": "tchssk",
        "website_url": "https://tchssk.github.io"
    }
}

ちゃんと "github""qiita" にそれぞれのレスポンスが入った形で返ってきました。

レスポンスの操作

KrakenD は、バックエンドから受け取った情報をクライアントにレスポンスする前に以下の方法で操作することができます。

  • Merging
  • Filtering
  • Grouping
  • Mapping
  • Capturing

では順番に試してみましょう。

Merging

複数のバックエンドからのレスポンスをマージすることができます。

  "endpoints": [
    {
      "endpoint": "/users/{id}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/api/v2/users/{id}",
          "encoding": "json",
          "host": [
            "https://qiita.com"
          ]
        },
        {
          "url_pattern": "/users/{id}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ]
        }
      ]
    }
  ]

上記のように設定すると GitHub と Qiita からのレスポンスがマージされます。

{
  "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4",
  "bio": null,
  "blog": "https://tchssk.github.io",
  "company": null,
  "created_at": "2015-05-05T15:43:31Z",
  "description": "A web developer who loves Ginger Ale !!",
  "email": null,
  "events_url": "https://api.github.com/users/tchssk/events{/privacy}",
  "facebook_id": "",
  "followees_count": 0,
  "followers": 6,
  "followers_count": 2,
  "followers_url": "https://api.github.com/users/tchssk/followers",
  "following": 3,
  "following_url": "https://api.github.com/users/tchssk/following{/other_user}",
  "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}",
  "github_login_name": "tchssk",
  "gravatar_id": "",
  "hireable": null,
  "html_url": "https://github.com/tchssk",
  "id": 12257128,
  "items_count": 11,
  "linkedin_id": "",
  "location": "Tokyo, Japan",
  "login": "tchssk",
  "name": "Taichi Sasaki",
  "organization": "",
  "organizations_url": "https://api.github.com/users/tchssk/orgs",
  "permanent_id": 83093,
  "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/83093/profile-images/1473702659",
  "public_gists": 0,
  "public_repos": 33,
  "received_events_url": "https://api.github.com/users/tchssk/received_events",
  "repos_url": "https://api.github.com/users/tchssk/repos",
  "site_admin": false,
  "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions",
  "twitter_screen_name": "tchssk",
  "type": "User",
  "updated_at": "2017-12-05T10:08:00Z",
  "url": "https://api.github.com/users/tchssk",
  "website_url": "https://tchssk.github.io"
}

フィールド名が重複している場合はどちらか片方のレスポンスが出力されるようです。

Filtering

2 種類のフィルタリングが利用できます。

Blacklisting

blacklist に指定されたフィールドを除外することができます。

  "endpoints": [
    {
      "endpoint": "/users/{id}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/users/{id}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ],
          "blacklist": [
            "avatar_url",
            "bio",
            "blog",
            "company"
          ]
        }
      ]
    }
  ]

上記のように設定すると avatar_urlbioblogcompany が除外されます。

{
  "created_at": "2015-05-05T15:43:31Z",
  "email": null,
  "events_url": "https://api.github.com/users/tchssk/events{/privacy}",
  "followers": 6,
  "followers_url": "https://api.github.com/users/tchssk/followers",
  "following": 3,
  "following_url": "https://api.github.com/users/tchssk/following{/other_user}",
  "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}",
  "gravatar_id": "",
  "hireable": null,
  "html_url": "https://github.com/tchssk",
  "id": 12257128,
  "location": "Tokyo, Japan",
  "login": "tchssk",
  "name": "Taichi Sasaki",
  "organizations_url": "https://api.github.com/users/tchssk/orgs",
  "public_gists": 0,
  "public_repos": 33,
  "received_events_url": "https://api.github.com/users/tchssk/received_events",
  "repos_url": "https://api.github.com/users/tchssk/repos",
  "site_admin": false,
  "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions",
  "type": "User",
  "updated_at": "2017-12-05T10:08:00Z",
  "url": "https://api.github.com/users/tchssk"
}

Whitelisting

whitelist に指定されたフィールドだけを出力することができます。

  "endpoints": [
    {
      "endpoint": "/users/{id}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/users/{id}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ],
          "whitelist": [
            "subscriptions_url",
            "type",
            "updated_at",
            "url"
          ]
        }
      ]
    }
  ]

上記のように設定すると subscriptions_urltypeupdated_aturl だけが出力されます。

{
  "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions",
  "type": "User",
  "updated_at": "2017-12-05T10:08:00Z",
  "url": "https://api.github.com/users/tchssk"
}

Grouping

グルーピングを行うことができます。冒頭で扱った GitHub と Qiita の例が正にこれですね。

Mapping

バックエンドからのレスポンスを別名のフィールドにマッピングすることができます。

  "endpoints": [
    {
      "endpoint": "/users/{id}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/users/{id}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ],
          "mapping": {
            "blog": "personal_blog"
          }
        }
      ]
    }
  ]

上記のように設定すると blogpersonal_blogマッピングされます。

{
  "avatar_url": "https://avatars3.githubusercontent.com/u/12257128?v=4",
  "bio": null,
  "company": null,
  "created_at": "2015-05-05T15:43:31Z",
  "email": null,
  "events_url": "https://api.github.com/users/tchssk/events{/privacy}",
  "followers": 6,
  "followers_url": "https://api.github.com/users/tchssk/followers",
  "following": 3,
  "following_url": "https://api.github.com/users/tchssk/following{/other_user}",
  "gists_url": "https://api.github.com/users/tchssk/gists{/gist_id}",
  "gravatar_id": "",
  "hireable": null,
  "html_url": "https://github.com/tchssk",
  "id": 12257128,
  "location": "Tokyo, Japan",
  "login": "tchssk",
  "name": "Taichi Sasaki",
  "organizations_url": "https://api.github.com/users/tchssk/orgs",
  "personal_blog": "https://tchssk.github.io",
  "public_gists": 0,
  "public_repos": 33,
  "received_events_url": "https://api.github.com/users/tchssk/received_events",
  "repos_url": "https://api.github.com/users/tchssk/repos",
  "site_admin": false,
  "starred_url": "https://api.github.com/users/tchssk/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/tchssk/subscriptions",
  "type": "User",
  "updated_at": "2017-12-05T10:08:00Z",
  "url": "https://api.github.com/users/tchssk"
}

Caputuring

特定フィールドのオブジェクトだけを抽出することができます。ここでは GitHub APIリポジトリを取得するエンドポイントで試してみます。

$ http https://api.github.com/repos/golang/go

{
    "id": 23096959,
    "name": "go",
    "full_name": "golang/go",
    "owner": {
        "login": "golang",
        "id": 4314092,
        "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/golang",
        "html_url": "https://github.com/golang",
        "followers_url": "https://api.github.com/users/golang/followers",
        "following_url": "https://api.github.com/users/golang/following{/other_user}",
        "gists_url": "https://api.github.com/users/golang/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/golang/subscriptions",
        "organizations_url": "https://api.github.com/users/golang/orgs",
        "repos_url": "https://api.github.com/users/golang/repos",
        "events_url": "https://api.github.com/users/golang/events{/privacy}",
        "received_events_url": "https://api.github.com/users/golang/received_events",
        "type": "Organization",
        "site_admin": false
    },
    "private": false,
    "html_url": "https://github.com/golang/go",
    "description": "The Go programming language",
    "fork": false,
    "url": "https://api.github.com/repos/golang/go",
    "forks_url": "https://api.github.com/repos/golang/go/forks",
    "keys_url": "https://api.github.com/repos/golang/go/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/golang/go/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/golang/go/teams",
    "hooks_url": "https://api.github.com/repos/golang/go/hooks",
    "issue_events_url": "https://api.github.com/repos/golang/go/issues/events{/number}",
    "events_url": "https://api.github.com/repos/golang/go/events",
    "assignees_url": "https://api.github.com/repos/golang/go/assignees{/user}",
    "branches_url": "https://api.github.com/repos/golang/go/branches{/branch}",
    "tags_url": "https://api.github.com/repos/golang/go/tags",
    "blobs_url": "https://api.github.com/repos/golang/go/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/golang/go/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/golang/go/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/golang/go/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/golang/go/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/golang/go/languages",
    "stargazers_url": "https://api.github.com/repos/golang/go/stargazers",
    "contributors_url": "https://api.github.com/repos/golang/go/contributors",
    "subscribers_url": "https://api.github.com/repos/golang/go/subscribers",
    "subscription_url": "https://api.github.com/repos/golang/go/subscription",
    "commits_url": "https://api.github.com/repos/golang/go/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/golang/go/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/golang/go/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/golang/go/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/golang/go/contents/{+path}",
    "compare_url": "https://api.github.com/repos/golang/go/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/golang/go/merges",
    "archive_url": "https://api.github.com/repos/golang/go/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/golang/go/downloads",
    "issues_url": "https://api.github.com/repos/golang/go/issues{/number}",
    "pulls_url": "https://api.github.com/repos/golang/go/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/golang/go/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/golang/go/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/golang/go/labels{/name}",
    "releases_url": "https://api.github.com/repos/golang/go/releases{/id}",
    "deployments_url": "https://api.github.com/repos/golang/go/deployments",
    "created_at": "2014-08-19T04:33:40Z",
    "updated_at": "2017-12-07T12:12:01Z",
    "pushed_at": "2017-12-07T05:10:23Z",
    "git_url": "git://github.com/golang/go.git",
    "ssh_url": "git@github.com:golang/go.git",
    "clone_url": "https://github.com/golang/go.git",
    "svn_url": "https://github.com/golang/go",
    "homepage": "https://golang.org",
    "size": 166440,
    "stargazers_count": 35170,
    "watchers_count": 35170,
    "language": "Go",
    "has_issues": true,
    "has_projects": false,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "forks_count": 4781,
    "mirror_url": null,
    "archived": false,
    "open_issues_count": 3158,
    "license": {
        "key": "bsd-3-clause",
        "name": "BSD 3-clause \"New\" or \"Revised\" License",
        "spdx_id": "BSD-3-Clause",
        "url": "https://api.github.com/licenses/bsd-3-clause"
    },
    "forks": 4781,
    "open_issues": 3158,
    "watchers": 35170,
    "default_branch": "master",
    "organization": {
        "login": "golang",
        "id": 4314092,
        "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/golang",
        "html_url": "https://github.com/golang",
        "followers_url": "https://api.github.com/users/golang/followers",
        "following_url": "https://api.github.com/users/golang/following{/other_user}",
        "gists_url": "https://api.github.com/users/golang/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/golang/subscriptions",
        "organizations_url": "https://api.github.com/users/golang/orgs",
        "repos_url": "https://api.github.com/users/golang/repos",
        "events_url": "https://api.github.com/users/golang/events{/privacy}",
        "received_events_url": "https://api.github.com/users/golang/received_events",
        "type": "Organization",
        "site_admin": false
    },
    "network_count": 4781,
    "subscribers_count": 2583
}

Goリポジトリですね。これを KrakenD を通して取得します。

  "endpoints": [
    {
      "endpoint": "/repos/{owner}/{repo}",
      "method": "GET",
      "concurrent_calls": 1,
      "client_max_rate": 0,
      "querystring_params": [],
      "backend": [
        {
          "url_pattern": "/repos/{owner}/{repo}",
          "encoding": "json",
          "host": [
            "https://api.github.com"
          ],
          "target": "owner"
        }
      ]
    }
  ]

上記のように設定すると owner のオブジェクトだけが抽出されます。

{
  "avatar_url": "https://avatars3.githubusercontent.com/u/4314092?v=4",
  "events_url": "https://api.github.com/users/golang/events{/privacy}",
  "followers_url": "https://api.github.com/users/golang/followers",
  "following_url": "https://api.github.com/users/golang/following{/other_user}",
  "gists_url": "https://api.github.com/users/golang/gists{/gist_id}",
  "gravatar_id": "",
  "html_url": "https://github.com/golang",
  "id": 4314092,
  "login": "golang",
  "organizations_url": "https://api.github.com/users/golang/orgs",
  "received_events_url": "https://api.github.com/users/golang/received_events",
  "repos_url": "https://api.github.com/users/golang/repos",
  "site_admin": false,
  "starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/golang/subscriptions",
  "type": "Organization",
  "url": "https://api.github.com/users/golang"
}

その他の機能

今回は試しませんでしたが、一般的な API Gateway にあるような以下の機能も備えているようです。

  • 複数のバックエンドへリクエストを分散する Load balancing
  • 一定時間あたりのリクエスト数を制限する Rate limits
  • 障害時などにバックエンドへのリクエストを遮断する Circuit breaker

性能

ベンチマークによると秒間処理リクエスト数は 3500 程度で、 OSSAPI Gateway として有名な Kong の約 2 倍だそうです。

f:id:tchssk:20171208122904p:plain

利用プラン

公式ページに書かれている以下の 3 種類のプランがあるようです。

  • Open Source
  • Free
  • Enterprise

KrakenD のコア機能は OSS として GitHub で公開されていて、好きな Web framework と組み合わせて使えるようになっています。それがおそらく Open Source プラン。 DockerHub のイメージや RPM などを介してビルド済みのものを利用するのが Free プランでしょう。 Enterprise には mailto: のボタンが置かれているだけなので、まだ本格的なマネタイズは行なっていないのかもしれません。

今回は少し触ってみた程度ですが、 API Gateway が必要になったときはもう少し詳しく検証して利用を検討してみたいと思いました。