Edited at

Goa v2を使った開発環境テンプレート


更新


変更点


  • 自動生成コードの修正用shell

  • api designの構成


今後の更新予定

需要がありそうだったら以下も追加していくかも


  • api認証を加える

  • grpcとか使ったサンプル

  • go moduleに移行


はじめに

この記事ではgoa v2を使った開発を紹介します。goaはマイクロサービスを構築するためgolangのフレームワークです。goaではDSLでAPIデザインを書くことで、それを元にドキュメントやクライアントツール、コントローラ(雛形)などを自動生成してくれます。

goaの利点をまとめると以下2つが大きいと思います。

- ビジネスロジックの開発に集中できる

- swaggerフォーマットのAPI定義ファイルの生成

swaggerの定義ファイルが手に入るので、これを利用した便利なツールを使うことで、APIドキュメントの自動生成(swagger-ui)や管理画面の作成(viron)を行うことができます。

v2は以前ベータ版という状況で、記事もあまり出ていないのでサンプルとして紹介します。

※ベータ版ですが、これからgoaをやり始める人はv2から始めた方が良いと思います


goa v2 is currently in beta: it is robust enough to be used in production but there may still be breaking changes before the final release. If you're new to goa then you may want to consider starting with v2.


goaの導入から、swagger-ui、viron、データベース(mysql)との連携までを行い、サンプルのAPI開発までの流れを書いていきます。

コードは以下のリポジトリに公開しています。

https://github.com/tonouchi510/goa2-sample


環境構築

golangとdockerはインストール済みである事を想定します。


ディレクトリ構成

$ tree -d -L 1

.
├── cmd # goaで自動生成される。
├── controller # goaで雛形が生成される。ビジネスロジックを書き加える。
├── design # APIデザインファイルを置く
├── docker # dockerコンテナ用の設定ファイルを置く
├── gen # goaで自動生成される。ここはいじる事はない。
├── readme # readme用の画像
├── script # コード修正用のscript等
├── server # 開発者、管理者用サーバなどのdir
└── vendor # depによってvendoringされたパッケージ置き場


APIサーバ


1. goaのインストールをします。

$ go get -u goa.design/goa/...


2. APIデザインを書く

goaのDSLでAPIデザインを書いていきます。まずはじめにAPI全体に関する設定を定義するDSLを書きます。


design/api_definition.go

import (

. "goa.design/goa/dsl"
cors "goa.design/plugins/cors/dsl"
)

// API describes the global properties of the API server.
var _ = API("goa2-sample", func() {
Title("SampleAPI")
Description("goa2 sample code.")
Version("1.0")
Contact(func() {
Name("tonouchi510")
Email("tonouchi27@gmail.com")
URL("https://github.com/tonouchi510/goa2-sample/issues")
})
Docs(func() {
Description("wiki")
URL("https://github.com/tonouchi510/goa2-sample/wiki")
})

cors.Origin("/.*localhost.*/", func() {
cors.Headers("content-type")
cors.Methods("GET", "POST", "PUT", "DELETE", "OPTIONS")
cors.MaxAge(600)
})

Server("goa2-sample", func() {
Services("Users", "Viron", "Admin")
Host("localhost", func() {
Description("development host")
URI("http://localhost:8080")
})
})
})


ここではこのサービスの説明やバージョン、ホストなどを記述します。CORSはとりあえずこのようにしています。


サンプルAPI

次に、designディレクトリ以下に追加したいAPIのデザインファイルを作っていきます。ここではサンプルで用意したusersの例で手順を説明します。

usersのAPIデザインは以下のようにします。基本的なCRUD処理をするAPIです。


design/users.go

// サービスの定義

var _ = Service("Users", func() {
Description("users serves user account relative information.")

HTTP(func() {
Path("/api/v1/users")
})

Method("list user", func() {
Description("List all stored users")

Result(CollectionOf(UserResponse))
HTTP(func() {
GET("/")
Response(StatusOK)
})
})

Method("get user", func() {
Description("Show user by ID")

Payload(GetUserPayload)
Result(UserResponse)

HTTP(func() {
GET("/{id}")
Response(StatusOK)
})
})

Method("create user", func() {
Description("Add new user and return its ID.")

Payload(CreateUserPayload)
Result(String)

HTTP(func() {
POST("/")
Response(StatusCreated)
})
})

Method("update user", func() {
Description("Update user item.")
Payload(UpdateUserPayload)
Result(UserResponse)
HTTP(func() {
PUT("/{id}")
Response(StatusOK)
})
})

Method("delete user", func() {
Description("Delete user by id.")
Payload(DeleteUserPayload)
HTTP(func() {
DELETE("/{id}")
Response(StatusNoContent)
})
})
})


ResponseやPayloadは、全てmedia_typeで定義して管理しやすいようにしておきます。

swagger-uiで見たときにResponseとPayloadの型とか例が見れてわかりやすいです。


3. コード自動生成

goaのコードジェネレータを使ってAPIデザイン定義からコードの雛形を自動生成します。

$ make goagen

# goa example およびコード修正処理
$ make regen

goa gen、goa example以外にも必要な処理を行なっているのでMakefile見て確認してください

※記事の下の方で説明します


4. ビジネスロジックの実装

自動生成できない部分(アプリケーション固有の処理部分)の実装を行います。

基本的にgenディレクトリ以下はいじる必要がなく、goa exampleで生成されるcontrollerやmain.goにコード追加、修正を加えていきます。


controller

サンプルAPIのcontrollerの雛形はこのようになっています。


controller/users.go

// Users service example implementation.

// The example methods log the requests and return zero values.
type userssrvc struct {
logger *log.Logger
}

// NewUsers returns the Users service implementation.
func NewUsers(logger *log.Logger) users.Service {
return &userssrvc{logger}
}

// List all stored users
func (s *userssrvc) ListUser(ctx context.Context) (res users.Goa2SampleUserCollection, err error) {
s.logger.Print("users.list user")
return
}

// Show user by ID
func (s *userssrvc) GetUser(ctx context.Context, p *users.GetUserPayload) (res *users.Goa2SampleUser, err error) {
res = &users.Goa2SampleUser{}
s.logger.Print("users.get user")
return
}

// Add new user and return its ID.
func (s *userssrvc) CreateUser(ctx context.Context, p *users.CreateUserPayload) (res string, err error) {
s.logger.Print("users.create user")
return
}

// Update user item.
func (s *userssrvc) UpdateUser(ctx context.Context, p *users.UpdateUserPayload) (res *users.Goa2SampleUser, err error) {
res = &users.Goa2SampleUser{}
s.logger.Print("users.update user")
return
}

// Delete user by id.
func (s *userssrvc) DeleteUser(ctx context.Context, p *users.DeleteUserPayload) (err error) {
s.logger.Print("users.delete user")
return
}


これに基本的なCRUD処理を加えていきます。ここで、DBアクセスをどうするかで色々な方法が考えられると思いますが、各Controllerのインスタンス生成の際にmain.goから引数でDBクライアントを渡して、Controllerはそれぞれのメソッドでそれを利用してDBアクセスするようにします。

コードは以下のようになります。


controller/users.go

import (

"context"
"database/sql"
"log"

users "github.com/tonouchi510/goa2-sample/gen/users"
)

// Users service example implementation.
// The example methods log the requests and return zero values.
type userssrvc struct {
logger *log.Logger
DB *sql.DB
}

// NewUsers returns the Users service implementation.
func NewUsers(logger *log.Logger, db *sql.DB) users.Service {
return &userssrvc{logger, db}
}

// List all stored users
func (s *userssrvc) ListUser(ctx context.Context) (res users.Goa2SampleUserCollection, err error) {
res = users.Goa2SampleUserCollection{}
s.logger.Print("users.List")

rows, err := s.DB.Query("SELECT id, name, email FROM users")
defer rows.Close()
if err != nil {
return
}
for rows.Next() {
var id, name, email string
user := &users.Goa2SampleUser{}
err = rows.Scan(&id, &name, &email)
if err != nil {
return
}
user.ID = id
user.Name = name
user.Email = email
res = append(res, user)
}
return
}

// Show user by ID
func (s *userssrvc) GetUser(ctx context.Context, p *users.GetUserPayload) (res *users.Goa2SampleUser, err error) {
s.logger.Print("users.Get")

var id, name, email string
err = s.DB.QueryRow("SELECT id, name, email FROM users WHERE id=?", p.ID).Scan(&id, &name, &email)
if err != nil {
return
}
res = &users.Goa2SampleUser{ID: id, Name: name, Email: email}
return
}

// Add new user and return its ID.
func (s *userssrvc) CreateUser(ctx context.Context, p *users.CreateUserPayload) (res string, err error) {
s.logger.Print("users.Create")
stmt, err := s.DB.Prepare("INSERT INTO users(id, name, email) VALUES(?,?,?)")
defer stmt.Close()
if err != nil {
return
}
_, err = stmt.Exec(p.ID, p.Name, p.Email)
if err != nil {
return
}
s.logger.Printf("INSERT: ID: %s | Name: %s | Email: %s", p.ID, p.Name, p.Email)
res = p.ID
return
}

// Update user item.
func (s *userssrvc) UpdateUser(ctx context.Context, p *users.UpdateUserPayload) (res *users.Goa2SampleUser, err error) {
res = &users.Goa2SampleUser{}
s.logger.Print("users.Update")
var stmt *sql.Stmt
if p.Name != nil && p.Email != nil {
stmt, err = s.DB.Prepare("UPDATE users SET name=?, email=? WHERE id=?")
_, err = stmt.Exec(p.Name, p.Email, p.ID)
} else if p.Name != nil {
stmt, err = s.DB.Prepare("UPDATE users SET name=? WHERE id=?")
_, err = stmt.Exec(p.Name, p.ID)
} else if p.Email != nil {
stmt, err = s.DB.Prepare("UPDATE users SET email=? WHERE id=?")
_, err = stmt.Exec(p.Email, p.ID)
}
if err != nil {
return
}
defer stmt.Close()

var id, name, email string
err = s.DB.QueryRow("SELECT id, name, email FROM users WHERE id=?", p.ID).Scan(&id, &name, &email)
if err != nil {
return
}
res = &users.Goa2SampleUser{ID: id, Name: name, Email: email}
s.logger.Printf("UPDATE: ID: %s | Name: %s | Email: %s", id, name, email)
return
}

// Delete user by id.
func (s *userssrvc) DeleteUser(ctx context.Context, p *users.DeleteUserPayload) (err error) {
s.logger.Print("users.Delete")
stmt, err := s.DB.Prepare("DELETE FROM users WHERE id=?")
defer stmt.Close()
if err != nil {
return
}
_, err = stmt.Exec(p.ID)
if err != nil {
return
}
s.logger.Printf("DELETE: Id: %s", p.ID)
return
}



main.go

main.goもコントローラにsqlクライアントを渡すように修正します。


cmd/goa2_sample/main.go

// importに以下を追加

"database/sql"
_ "github.com/go-sql-driver/mysql"

// Initialize service dependencies such as databases.

var (
db *sql.DB
var err error
)
{
db, err = sql.Open("mysql", "test:test@/sampledb")
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
}

usersSvc = goa2sample.NewUsers(logger, db)

これで基本的なCRUD処理をするAPIは完成です。自分で各部分はこの程度で済みます!

更新前の記事:


デメリットとしてはgoa自動生成されたコードを自分で修正しないといけない部分が多いです。goa v2ではDB用のプラグインであるgormaもまだ対応していないので、この辺めんどくさくなっています。修正自動化できる部分増やしたら追記していきます。。。


力技ですがとりあえず修正スクリプト追加しました(macでしか確認してません)。

新しいServiceをマウントする必要がある場合はスクリプトに他の例に合わせて追加する必要はあります。

※もっと綺麗にできたら更新します

dbClient対応以外にも、独自のhandler追加できるように若干修正したりしています。

独自のhandlerでは、現在は開発者用のswagger-uiが追加されています。その他必要なものができた場合にも同様に追加できます。


mysql

DBサーバはdockerコンテナで起動します。docker-compose.ymlに以下の記述をします。


docker-compose.yml


db:
image: mysql:5.7
command:
mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --skip-character-set-client-handshake
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: "Asia/Tokyo"
ports:
- "3306:3306"
volumes:
- "./docker/mysql:/docker-entrypoint-initdb.d"

環境変数は.envファイルで設定します。サンプルはリポジトリに公開していますが、設定を変えたらpushしないように注意してください。


.env

MYSQL_ROOT_PASSWORD=root

MYSQL_DATABASE=sampledb
MYSQL_USER=test
MYSQL_PASSWORD=test

また、mysqlイメージのコンテナでは、docker--entrypoint-initdb.d以下にsqlやshellをマウントすることで起動時にそれらを実行してくれるようになります。データベースやテーブル作成のsqlを用意してマウントするようにすると便利です。今回はdocker/mysql以下に設定ファイルを置いてあります。


swagger-ui

前の記事ではnginxで別のwebサーバを用意してswagger-uiを起動していましたが、goのapiサーバと一緒のオリジンでhandleするように変更してます。


cmd/goa2_sample/http.go

func newDevConsoleHandler(pathPrefix string, directory string) http.HandlerFunc {

return func(w http.ResponseWriter, r *http.Request) {
fs := http.FileServer(http.Dir(directory))
http.StripPrefix(pathPrefix, fs).ServeHTTP(w, r)
}
}

これをhandle


cmd/goa2_sample/http.go

http.HandleFunc("/_dev/console/", newDevConsoleHandler("/_dev/console/","./server/swagger-ui/"))


これでlocalhost:8080/_dev/console/でgoa2-sample用のswagger-ui(APIドキュメントサイト)が起動します。WEBフロント側はこれを見て開発していく感じになります。


viron

管理画面、ダッシュボードのフロントコードフリーを実現してくれます。


修正点


  • ちゃんとAdmin用のAPIを使うように変更(認証は後で追加)

Dockerfileは以下のようになります。


Dockerfile

# viron用イメージ

FROM node:9 as viron

# Setup project
RUN git clone https://github.com/cam-inc/viron.git /viron
RUN sed -i "s|ssl: true,|ssl: false,|g" /viron/rollup.local.config.js

RUN chown -R node:node /viron
ENV HOME /viron
USER node
WORKDIR $HOME

RUN npm install

EXPOSE 8080
USER root
CMD npm start


開発環境用なので、ssl無し(http)でアクセスできるように修正しています(6行目)。

vironのドキュメントや必須API等については以下のサイトを参考にしてください。

https://cam-inc.github.io/viron-doc/docs/dev_api.html

https://techblog.istyle.co.jp/archives/246

vironの管理画面やダッシュボードに追加したいエンドポイントは以下のようにVironMenuで設定を加えていきます(記事の下の方にこの場合のvironのダッシュボードと管理画面の例があります)。


controller/viron.go

// Add viron_menu

func (s *vironsrvc) VironMenu(ctx context.Context) (res *viron.VironMenu, err error) {
res = &viron.VironMenu{}
s.logger.Print("viron.viron_menu")
cl := "green"
th := "standard"
pk := "id"
pagenation := true

res = &viron.VironMenu{
Name: "goa2-sample Admin Screen",
Tags: []string{
"local",
},
Color: &cl,
Theme: &th,
Pages: []*viron.VironPage{
&viron.VironPage{
Section: "dashboard",
Name: "ダッシュボード",
ID: "quickview",
Components: []*viron.VironComponent{
&viron.VironComponent{
Name: "Users(bar)",
API: &viron.VironAPI{
Method: "get",
Path: "/api/v1/admin/user_number",
},
Style: "graph-bar",
},
},
},
&viron.VironPage{
Section: "manage",
ID: "user-admin",
Name: "ユーザ管理",
Components: []*viron.VironComponent{
&viron.VironComponent{
API: &viron.VironAPI{
Method: "get",
Path: "/api/v1/admin/users",
},
Name: "ユーザ一覧",
Style: "table",
Primary: &pk,
Pagination: &pagenation,
Query: []*viron.VironQuery{
&viron.VironQuery{
Key: "id",
Type: "integer",
},
&viron.VironQuery{
Key: "name",
Type: "string",
},
&viron.VironQuery{
Key: "email",
Type: "string",
},
&viron.VironQuery{
Key: "created_at",
Type: "string",
},
&viron.VironQuery{
Key: "updated_at",
Type: "string",
},
},
TableLabels: []string{
"id",
"name",
"email",
"created_at",
"updated_at",
},
},
},
},
},
}
return
}



実行

docker-compose.ymlに各コンテナの設定を記述しているので、以下のコマンドで起動します。

$ docker-compose build

$ docker-compose up -d

これで、


  • ポート3306にmysql

  • ポート8000にviron

がそれぞれ起動している状態になります。

次に、apiサーバのビルド&実行をします。

$ cd cmd/goa2_sample/

$ go build
$ cd -
$ ./cmd/goa2_sample/goa2_sample

これでポート8080でapiサーバが起動している状態になります。swagger-uiも起動します。

※vironは最初にhttp://localhost:8080/v1/swagger.jsonを追加する必要有り

各サーバにアクセスした時のスクショを載せておきます。


swagger-ui

image.png


viron

image.png

image.png


まとめ

goa v2を使った開発の流れを書いていきました。swaggerフォーマットのAPI仕様書が手に入ることによる恩恵を、出来るだけ得られるようにした構成になっています。サンプルをクローンしてreadmeに従って環境構築すれば、すぐにswagger-uiとviron、mysqlが連携済みのgoaで開発を進められます。気に入ったら開発環境テンプレートとして利用していただけたら幸いです。

※何かコメント等ありましたらよろしくお願いします。