Ponz Dev Log

ゆるくてマイペースな開発日記

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

ご無沙汰してます。約2ヶ月ぶりの投稿になります。

ここ2ヶ月くらいは社内のイベント向けのアプリを作ったり、Kubernetes勉強会に参加して初めてAWS触ったり、コンテナ周りで新人向けに講師としてスピーカーやったりと技術(特にDockerコンテナ)漬けの日々を過ごしておりました。(Kubernetes@AWSは今度記事にする予定です)

お陰様で今日の健康診断で2ヶ月で10kg体重落ちてました汗
技術だけじゃなく美味しいご飯どころも探さなきゃね。。。

さて、4月末から連休で時間ができたので新しいものに挑戦しようとGoに触り始めました。 折角なら馴染みのあるWebアプリを作成して、Dockerコンテナにまとめるくらいはやろうと全速力でやったので記事投下です。

ソースコード見たいよって方は以下のリンクからどうぞ。

github.com

Go出発点

今まで手を出そうと思いつつ何も手をつけていなかったので、Goについては知識0からスタート。 比較的最近出版された本ならばバージョンの違いでつまづくことは少ないはずなので、とりあえず『スターティングGo言語』買って3日ほどで手を動かしつつ読了。あとは作りたいものを完成させます。

www.shoeisha.co.jp

さっと書店で手に取った本にしてはアタリだった気がします。 全7章建てで、基本で小分けに5章割いて、残りはツールやパッケージの話で2章。1つ1つのGoの仕組みを分解してコード例も沢山載せてくれているので、写経しているだけでもかなり書けるようになります。

Go製のWebアプリを作る

サーバー側は最低限文字列とJSON返すだけ返せればよかったので、簡易なものとしてピッタリな Echo を選択。 公式のドキュメントは分かりやすいし、サイトも見やすいので取っ付きやすかったです。

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

echo.labstack.com

以下、公式の例を使いつつルーティングとミドルウェアを設定した main.go 例です。
ルーティングのミドルウェアはデフォルトの設定を使うとJSON形式の見辛いログが出力されるので、ApacheCommon Log Format に似せて出力されるように設定してます。ログ出力は出力内容は柔軟に変えられるみたいですね。

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 としてルーティングしたときに返す関数をまとめてみました。

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コンテナにまとめる

関心があったのと同時に割と引っかかったのが、ここ。特にビルドイメージ大きすぎなのが難点でした。

  • GoはOS依存させないために、OSが標準で備えているようなライブラリを使わず、ランタイムとアプリで使うパッケージを全て実行ファイルの中に取り込みます。この性質があるため、少しコード量の多いアプリでも成果物が数百MBになるのもザラ。

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

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

docs.docker.com qiita.com

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

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

# 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ベースイメージ+マルチステージビルドのイメージでサイズは半分以下に抑えられています!やったね!

$ 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を選択しましたが、謳い文句の通りシンプルかつ効率的に開発&ビルドできたのはすごい...!!
バイナリ単位でアプリはまとめられるので、マイクロサービスと相性が良さそうな印象です。しかもブロックチェーンのHyperledger fabricもGoで開発できるようなので、次はGoを使ってブロックチェーンに挑もうかなと夢が膨らんだところで締めます。