Goで簡易Webアプリ作成&Dockerコンテナにまとめる

連休で少し時間ができたので新しいものに挑戦しようとGoに触り始めました。
折角なら馴染みのあるWebアプリを作成して、Dockerコンテナにまとめようっていう記事です。Goの文法やGOPATHのことは今回は載せてません。

この記事のソースコード見たいよって方は以下のリンクからどうぞ。
https://github.com/sasaken555/ponz_goecho_server

## Go製のWebアプリを作る

さっそく本題。Webアプリとしては最低限文字列とJSON返すだけでよかったので、簡易なものとしてピッタリな Echo を選択。公式のドキュメントは分かりやすいし、サイトも見やすいので取っ付きやすいです。

**[Echo]**
https://echo.labstack.com/:embed

"minimalist Go web framework" と謳うくらいだから、Node.jsでいうExpress.jsと同じような位置付けなのかな？

※ 参考までに...
* Echo(Go): `High performance, extensible, minimalist Go web framework`
* Express.js(Node.js): `Fast, unopinionated, minimalist web framework for Node.js`

以下、公式の例を使いつつルーティングとミドルウェアを設定した `main.go` 例です。  
ルーティングのミドルウェアはデフォルトの設定を使うとJSON形式の見辛いログが出力されるので、Apacheの [Common Log Format](https://httpd.apache.org/docs/current/ja/logs.html) に似せて出力されるように設定してます。ログ出力は出力内容は柔軟に変えられるみたいですね。

```go
package main

import (
	"net/http"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
	"github.com/sasaken555/ponz_goecho_server/routes"
)

func main() {
	/* Echoインスタンスの作成 */
	e := echo.New()

	/* Root Level Middleware */
	// ログ出力は Apache Common Log Format っぽく設定すると読みやすい
	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
		Format: "${host} [${time_rfc3339_nano}] \"${method} ${uri}\" ${status} ${bytes_in} ${bytes_out}\n",
	}))
	e.Use(middleware.Recover())

	/* ルーティングの設定 */
	// 第2引数の関数は別パッケージに外出しすると分かりやすい
	e.GET("/users/:id", routes.GetUser)
	e.GET("/users/json", routes.GetJSONUser)

	e.Logger.Fatal(e.Start(":1323")) // ポート1323で起動。
}

```

また、ルーティングの設定・関数は main に全部突っ込むと後で見づらくなるので、外出ししてあげます。今回は `routes/user.go` としてルーティングしたときに返す関数をまとめる。

```go
package routes

import (
	"net/http"
	"strconv"
	"github.com/labstack/echo"
	"github.com/sasaken555/ponz_goecho_server/util"
)

// GetUser ... Pathパラメータからユーザー(=ID)を取り出して返す
func GetUser(c echo.Context) error {
	// User ID from Path Parameter `users/:id`
	id := c.Param("id")
	return c.String(http.StatusOK, id)
}

// Customer ... 顧客情報の構造体
type Customer struct {
	ID        int64  `json:"id" xml:"id"`
	Name      string `json:"name" xml:"name"`
	OrderNum  int    `json:"ordernum" xml:"ordernum"`
	OrderProd string `json:"orderprod" xml:"orderprod"`
}

// GetJSONUser ... 顧客情報のJSONを返す
func GetJSONUser(c echo.Context) error {
	userID, err := strconv.ParseInt(c.QueryParam("userId"), 10, 0)  // strconvで文字列から整数に型変換
	userName := c.QueryParam("userName")
	orderNum := util.GetRand(100)  // 別のパッケージからインポートした指定桁数で乱数を返す関数を使う

	// userIDが整数でない(=型変換できない)ならば500エラーを返す
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "You Should Provide userId as Integer!")
	}

	// 構造体のポインタを作成
	u := &Customer{
		ID:        userID,
		Name:      userName,
		OrderNum:  orderNum,
		OrderProd: "Blend Coffee",
	}

	return c.JSON(http.StatusOK, u)
}

```

## Dockerコンテナにまとめる

Dockerコンテナにソース含めて全部入れてビルドすると悩むのが、ビルドイメージ大きすぎ問題。

* 実行ファイルのビルド時に、ランタイムとアプリで使うパッケージを全て実行ファイルの中に取り込むため、少しコード量の多いアプリでも成果物が数百MBになるのもザラ。

* 実行ファイルを組み込んだDockerコンテナのイメージがデカイとなると、`docker pull` の度にディスクと時間が取られるのでポータビリティの点から使いづらい。

上記の問題があって頭を抱えましたが、ベースイメージを小さいものにするのに加えて、Dockerのマルチステージビルドで対応することしました。

※ 参考記事
[Use multi-stage builds](https://docs.docker.com/v17.09/engine/userguide/eng-image/multistage-build)
[Docker multi stage buildで変わるDockerfileの常識](https://qiita.com/minamijoyo/items/711704e85b45ff5d6405)



通常だとソースコードを全てDockerコンテナに含めますが、Goは良くも悪くもソースコードから実行ファイルを作成するので、最終成果物の実行ファイルだけコンテナイメージに入っていればOKと考える。下のコード例の(1)が実行ファイル作成のビルド、(2)が(1)で作った実行ファイルをコピーして作った最終系のコンテナイメージになります。

ベースイメージも alpine イメージを使うことでさらに軽量化してます。

``` docker
　
# Full SDK version ... (1)
FROM golang:1.10-alpine AS build

RUN apk update && apk upgrade \
    && apk add curl git

RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

WORKDIR /go/src/github.com/sasaken555/ponz_goecho_server
COPY . .

RUN dep ensure
RUN go build -o ponz_goecho_server

# Final Output ... (2)
FROM golang:1.10-alpine
COPY --from=build /go/src/github.com/sasaken555/ponz_goecho_server/ponz_goecho_server /bin/ponz_goecho_server
CMD /bin/ponz_goecho_server

```

結果として、通常のベースイメージ+アプリソースを含めてビルドしたイメージ(tag: heavy)とalpineベースイメージ+マルチステージビルドのイメージ(tag: light)で比較するとサイズは半分以下に抑えられていますね！やったね！

```
　
$ docker image ls ponz_goecho_server

REPOSITORY           TAG                 IMAGE ID            CREATED              SIZE
ponz_goecho_server   light               24b61e6c4f83        7 seconds ago        386MB
ponz_goecho_server   heavy               20bac15a0acf        About a minute ago   908MB

```


Goの謳い文句通りシンプルかつ効率的に開発&ビルドできたのはすごい...!!