goaのコンテナを作ってみる

## 概要
goaの開発環境を作りたかったので、調べて作ってみる。

## 前提条件
- macOS
- Docker Toolbox ( Docker for mac )

## ファイル構成
手動で作成・修正する作成するファイルは、下記となります。

```:ファイル構成
golang-sample-api
　├ go
　│　└ Dockerfile
　├ src
　│  ├ Gododir
　│  │  └ main.go
　│  ├ Gopkg.lock
　│  ├ Gopkg.tomi
　│  └ vendor
　├ .gitignore
　└ docker-compose.yml
```

## goaコンテナ作成
### フォルダ作成
アプリケーションのコードが格納するフォルダを用意します。

```go/src/api
$ mkdir -p go/src
```

### .gitignore作成
下記のような感じで作成します。

```:.gitignore
# https://github.com/github/gitignore/blob/master/Go.gitignore

.idea
.DS_Store
vendor

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
godobin*

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

```

### Dockerfile作成
下記の内容で作成します。

```dockerfile:go/src/Dockerfile
FROM golang:1.11.4

# locale & timezone (Asia/Tokyo)
# https://github.com/moby/moby/issues/12084
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

# system update
RUN apt-get update && \
    apt-get install -y vim less

# copy application code from host.
ADD src /go/src/api
WORKDIR /go/src/api

# install package management tool (dep).
ARG DEP_RELEASE_TAG=v0.5.0
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

# ↓ここから下はあとでコメントを解除します
# install packages. (vendoring: /go/bin/api/vendor/)
#RUN dep ensure -v -vendor-only=true

# install development tools.
#RUN go install api/vendor/github.com/goadesign/goa/goagen

# build
#RUN go build -o vendor/server

#EXPOSE 8080
#CMD ["./vendor/server"]
```
### docker-compose.yml作成
下記の内容で作成します。

```yaml:docker-compose.yml
version: '3'
services:
  go:
    image: my/golang-sample-api
    build:
      context: ./
      dockerfile: ./go/Dockerfile
    ports:
      - 8080:8080
    volumes:
      - ./src:/go/src:cached
      # exclude volumes
      #- /go/src/api/vendor
      #- /go/src/api/swagger
    tty: true
    stdin_open: true
```

### 初期化（dep init）
下記コマンドにてパッケージ管理ツールを初期化。

```bash:コンソール
$ docker-compose run go dep init
```
※ Gopkg.lock, Gopkg.toml, vendorフォルダが自動生成される

### Gopkg.tomlファイル
`Gopkg.toml`ファイルを下記の内容で上書きする。

```toml:go/src/Gopkg.toml
required = [
    "github.com/goadesign/goa/goagen",
    "github.com/goadesign/goa/goagen/gen_app",
    "github.com/goadesign/goa/goagen/gen_main",
    "github.com/goadesign/goa/goagen/gen_client",
    "github.com/goadesign/goa/goagen/gen_controller",
    "github.com/goadesign/goa/goagen/gen_js",
    "github.com/goadesign/goa/goagen/gen_schema",
    "github.com/goadesign/goa/goagen/gen_swagger",
    "github.com/goadesign/goa/goagen/meta",
    "github.com/goadesign/goa/goagen/utils",
    "github.com/goadesign/goa/client",
    "github.com/goadesign/goa/cors",
    "github.com/goadesign/goa/design",
    "github.com/goadesign/goa/design/apidsl",
    "github.com/goadesign/goa/dslengine",
    "github.com/goadesign/goa/encoding",
    "github.com/goadesign/goa/goatest",
    "github.com/goadesign/goa/logging",
    "github.com/goadesign/goa/middleware",
    "github.com/goadesign/goa/uuid",
    "github.com/goadesign/goa/version",
    "github.com/goadesign/goa"
]

# https://asahima.hatenablog.jp/entry/2018/04/01/220000
[[override]]
  name = "github.com/satori/go.uuid"
  revision = "master"

[prune]
  go-tests = true
  unused-packages = true

[[constraint]]
  name = "github.com/goadesign/goa"
  version = "1.4.0"
```

### APIデザイン
`dep`でインストールしようとするとエラーになるので、`design.go`を先に作成します。

```golang:go/src/design/design.go
package design

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

var _ = API("cellar", func() {
    Title("The virtual wine cellar")
    Description("A simple goa service")
    Scheme("http")
    Host("localhost:8080")
})

var _ = Resource("bottle", func() {
    BasePath("/bottles")
    DefaultMedia(BottleMedia)

    Action("show", func() {
        Description("Get bottle by id")
        Routing(GET("/:bottleID"))
        Params(func() {
            Param("bottleID", Integer, "Bottle ID")
        })
        Response(OK)
        Response(NotFound)
    })
})

var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
    Description("A bottle of wine")
    Attributes(func() {
        Attribute("id", Integer, "Unique bottle ID")
        Attribute("href", String, "API href for making requests on the bottle")
        Attribute("name", String, "Name of wine")
        Required("id", "href", "name")
    })
    View("default", func() {
        Attribute("id")
        Attribute("href")
        Attribute("name")
    })
})

var _ = Resource("swagger", func() {
    Origin("*", func() {
        Methods("GET")
    })
    Files("/swagger.json", "swagger/swagger.json")
    Files("/swagger.yaml", "swagger/swagger.yaml")
})
```

### goagenインストール
パッケージ管理ツール(dep)を利用してインストールします。

```bash:ターミナル
$ docker-compose run go dep ensure -v
```

### goagenをビルドする
下記コマンドで「`vendor/github.com/goadesign/goa/goagen/goagen`」が作成される。

```bash:ターミナル
$ docker-compose run go bash -c "cd vendor/github.com/goadesign/goa/goagen && go build -v"
```
### コード自動生成
デザイン(design.go)の内容でアプリケーションを作成する。

```bash:ターミナル
$ docker-compose run go vendor/github.com/goadesign/goa/goagen/goagen bootstrap -d api/design
```

下記bottle.goをShowメソッドを下記内容に置き換える。

```go:src/bottle.go
~ 省略 ~

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
	if ctx.BottleID == 0 {
		return ctx.NotFound()
	}

	bottle := app.GoaExampleBottle{
		ID:   ctx.BottleID,
		Name: fmt.Sprintf("Bottle no.%d", ctx.BottleID),
		Href: app.BottleHref(ctx.BottleID),
	}

	return ctx.OK(&bottle)
}
```

### Dockerfile調整
コメントアウトされていた部分を解除する。

```dockerfile:go/Dockerfile
~ 省略 ~

# ↓ここから下はあとでコメントを解除します
# install packages. (vendoring: /go/bin/api/vendor/)
RUN dep ensure -v -vendor-only=true

# install development tools.
RUN go install api/vendor/github.com/goadesign/goa/goagen

# build
RUN go build -o vendor/server

EXPOSE 8080
CMD ["./vendor/server"]
```

### docker-compose.yml調整
コメントアウトされていた部分を解除する。
※ パフォーマンス改善のため、 vendor,swaggerフォルダをマウント対象から外す

```yaml:docker-compose.yml
    volumes:
      - ./src:/go/src:cached
      # exclude volumes
      - /go/src/api/vendor
      - /go/src/api/swagger
```

### dockerイメージ構築
Dockerfileの内容が変わったので再構築する。

```bash:コンソール
$ docker-compose build
```

### サーバ起動
下記コマンドにてdockerコンテナを起動する

```bash:コンソール
# コンテナ起動
$ docker-compose up -d

# サーバの状態確認
$ docker-compose ps

# サーバのログ確認 (ctrl + c で確認終了)
$ docker-compose logs -f --tail 10 go
```

### 動作確認
下記コマンドにてブラウザを起動し、閲覧できるか確認する

```bash:ターミナル
# コマンドで確認する場合 (200 ok.)
$ curl -i 192.168.99.100:8080/bottles/1
HTTP/1.1 200 OK
Content-Type: application/vnd.goa.example.bottle+json
Date: Fri, 01 Feb 2019 08:16:01 GMT
Content-Length: 50

{"href":"/bottles/1","id":1,"name":"Bottle no.1"}

# コマンドで確認する場合 (404 not found.)
$ curl -i 192.168.99.100:8080/bottles/0
HTTP/1.1 404 Not Found
Date: Fri, 01 Feb 2019 08:19:53 GMT
Content-Length: 0
```
※ `192.168.99.100` の部分は、dockerの環境によって異なります
※ サーバが起動するまでに少し時間がかかる場合がある

## godoとgolintを追加する
### サーバ停止
起動中のサーバを停止(破棄)します。

```bash:コンソール
$ docker-compose down
```

### Gopkg.tomlに追加する
パッケージ管理ツール(dep)で、`godo`と`golint`を追加インストールする

```toml:src/Gopkg.toml
required = [
    "gopkg.in/godo.v2",                       <- 追加
    "gopkg.in/godo.v2/cmd/godo",              <- 追加
    "golang.org/x/lint/golint",               <- 追加

~ 省略 ~
```

### Gododir/main.go作成
godoでwatchする対象のファイルを指定します。

```go:go/src/Gododir/main.go
package main

import (
    godo "gopkg.in/godo.v2"
)

func tasks(p *godo.Project) {

    p.Task("server", nil, func(c *godo.Context) {
        // rebuilds and restarts when a watched file changes
        c.Start("main.go", godo.M{"$in": "./"})
    }).Src("*.go", "**/*.go").
        Debounce(3000)
}

func main() {
    godo.Godo(tasks)
}
```

### Dockerfile追記
godo、golintコマンドが使えるように`install development tools`に追加する。
また、CMDで実行されるコマンドを`godo sever --watch`に差し替える

```dockerfile:go/src/Dockerfile
# install development tools.
RUN go install api/vendor/github.com/goadesign/goa/goagen && \  <- 「&& \」を最後に追加
    go install api/vendor/golang.org/x/lint/golint/ && \        <- 追加
    go install api/vendor/gopkg.in/godo.v2/cmd/godo             <- 追加

# build
RUN go build -o vendor/server

EXPOSE 8080
# CMD ["./vendor/server"]                                       <- コメントアウト
CMD ["godo", "server", "--watch"]                               <- 追加
```

### dockerイメージ再構築
Dockerfileを変更したので、dockerイメージを再構築する。

```bash:コンソール
$ docker-compose build
```

### サーバ起動
下記コマンドにてdockerのコンテナを起動してサーバを起動します。

```bash:ターミナル
# コンテナ起動
$ docker-compose up -d

# コンテナの状態確認
$ docker-compose ps

# コンテナのログ確認 (ctrl + c で終了)
$ docker-compose logs -f --tail 10 go
```

### 動作確認
下記コマンドにてブラウザを起動し、閲覧できればOK。

```bash:コンソール
# ブラウザで確認する場合
$ open http://192.168.99.100:8080/bottles/1

# swagger.jsonを確認する場合
$ open http://192.168.99.100:8080/swagger.json
```

## その他
### パッケージ(Gopkg.toml)を変更した場合
下記のように、depコマンドの実行が可能です。

```bash:コンソール
$ docker-compose exec go dep ensure -v -vendor-only=true
```

### デザイン(design/design.go)を変更した場合
下記のように、gogenコマンドの実行が可能です。

```bash:コンソール
$ docker-compose exec go goagen bootstrap -d api/design
```
### golintの実行
下記のような感じで実行可能です。

```bash:コンソール
$ docker-compose exec go golint ./...

# vendorフォルダを除外する
$ docker-compose exec go golint $(go list ./... | grep -v /vendor/)
```

以上

## 参考サイト
- [依存関係管理ツールdep(golang)](https://qiita.com/Azizjan/items/66564b5dc7597717932b)
- [github - goadesign/goa](https://github.com/goadesign/goa)
- [goa をはじめる](https://goa.design/ja/learn/guide/)
- [goa API デザイン言語](https://goa.design/ja/design/overview/)
- [GolangのgoaでAPIをデザインしよう（基本編）](https://tikasan.hatenablog.com/entry/2017/05/05/212501)
- [GolangのgoaでAPIをデザインしよう（Model編）](https://tikasan.hatenablog.com/entry/2017/05/15/172535)
- [goaを使ってAPIの設計・開発し、OpenAPIドキュメントを自動生成](https://kuroeveryday.blogspot.com/2018/05/goa-design-first-api-generation.html)
- [これからGo言語を書く人への三種の神器](https://qiita.com/osamingo/items/d5ec42fb8587d857310a)
- [Go の CI で Lint と カバレッジ回して非人間的なレビューは自動化しよう](http://haya14busa.com/ci-for-go-in-end-of-2016/)
- [GoLandでGo言語のLinterを設定する](https://qiita.com/m_nakamura145/items/a7ef77a41fba67205710)
- [now you can run golint $(go list ./... | grep -v /vendor/)](https://www.reddit.com/r/golang/comments/5ry44g/now_you_can_run_golint_go_list_grep_v_vendor/)