GoとDockerとMicroservices 最初の一歩

この記事は [Go その2 Advent Calendar 2015](http://qiita.com/advent-calendar/2015/go2) 10日目の記事です。

今回はgoとdockerを使ってMicroserviceっぽく、ウェブアプリケーションを作っていく時のさわりの部分を書いてみました。

# Microservice Hello World

macで[docker toolbox](https://www.docker.com/docker-toolbox)インストールして、`docker-machine start dev`とdocker-machineを立ち上げた後から。

`Hello World`を返すウェブサーバーのdockerイメージを用意してみます。

```go:helloworld.go
package main

import "net/http"

func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World"))
  })
  http.ListenAndServe(":8080", nil)
}
```

あらかじめビルド。

```bash
$ GOOS=linux GOARCH=amd64 go build helloworld.go
```

Dockerfileはこんな感じ。

```:Dockerfile
FROM busybox

COPY ./helloworld /helloworld
CMD ["/helloworld"]
```

docker build。

```bash
$ docker build -t helloworld .
```

あとはイメージを実行するだけです。

```bash
$ docker run -p "8080:8080" --rm helloworld
$ curl http://$(docker-machine ip dev):8080
Hello World
```

## dockerイメージの容量を小さくする

https://hub.docker.com/_/busybox/

dockerイメージはできるだけ小さくするようにするのが一つのプラクティスのようです。
上で使っているbusyboxは最小のパッケージですが、単純にgoを実行するだけならばこれで十分です。

dockerを使った運用を行う際に https://github.com/docker/distribution などに
イメージを登録したり、イメージを引っ張ってきたりするのですが、容量が大きいとそのぶん時間がかかります。

RUNなどを[ワンライナーで書いている](https://github.com/tianon/docker-brew-ubuntu-core/blob/3c355946fd5164da3f31063a5c5f249c826f7071/trusty/Dockerfile)のもビルドを早くしたり、容量を下げたりする方法のひとつです。

```
RUN echo '#!/bin/sh' > /usr/sbin/policy-rc.d \
	&& echo 'exit 101' >> /usr/sbin/policy-rc.d \
	&& chmod +x /usr/sbin/policy-rc.d \
```

少し油断すると、すぐに容量が肥大化するので注意しておくことのひとつになります。

参考: [(より)小さいDockerイメージを作ろう](https://www.ianlewis.org/jp/small-docker-images)


# 環境ごとに動作を変える

開発環境・ステージング環境・本番環境を用意する場合、その環境ごとにコンフィグの設定を用意して、起動時に引数などに設定して実行することがあります。
Railsの場合なら `rails server -e production` と環境を指定し、
`config/environments/<ENV>.rb`ファイルを読んで環境ごとに設定を変更する、といったところです。

これは便利ではあるのですが、設定ファイルのルールをきちんと決めておかないと、各所に散乱してしまったり、設定の仕方がフレームワークに依存してしまったり、リポジトリ内に外部に公開できない情報が含まれてしまうという問題もあったりします。

特にマイクロサービスの場合、小さな機能を持つサービスをいくつも立ち上げることが前提です。設定ファイルに依存していると、まず元のリポジトリにファイルを追加をしてから、そのイメージをビルドして、デプロイ作業を何度もやらなくてはいけなくなります。

`development` がもうひとつ必要になったときに `development002`といった環境名を逐一つけなければいけないのも大変です。

そしてそのビルドされたイメージの設定がどうなっているかを知る術が欲しくなったりします。Goの場合は、ビルドした実行ファイルを置くだけで良かったのが環境ファイルもイメージに入れて実行時に参照など最初の理想から離れてしまいます。

そんな経緯があるのか http://12factor.net/config では、設定は環境変数に入れる方法を取り上げています。

# サービス間の連携をする

マイクロサービスなので、サービス間で通信することになります。

例えば上で作成したhelloworldにアクセスするのに、
ひとつ間に挟む gateway というコンテナを挟んでみます。

```docker:Dockerfile
FROM busybox

COPY ./gateway /gateway

CMD ["/gateway"]
```

```go:gateway.go
package main

import (
	"io/ioutil"
	"net/http"
	"os"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		urlStr := os.Getenv("API_URL")
		req, err := http.NewRequest(r.Method, urlStr, r.Body)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write([]byte(err.Error()))
			return
		}

		cli := http.Client{}
		resp, err := cli.Do(req)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write([]byte(err.Error()))
			return
		}
		defer resp.Body.Close()

		bs, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write([]byte(err.Error()))
			return
		}

		w.WriteHeader(resp.StatusCode)
		w.Write(append([]byte("Gateway -> "), bs...))
	})
	http.ListenAndServe(":8080", nil)
}
```

`gateway.go`はhttpリクエストを受け取ったら、環境変数 `API_URL` にリクエストを送るというものです。helloworld.goの場合と同じように `go build`をあらかじめして、それをDockerイメージの中に入れます。

そしたら、各イメージのコンテナを作成します。

```bash
$ docker run -d --name helloworld helloworld
$ docker run -d --name gateway \
    -p "8080:8080"      \
    --link="helloworld" \
    -e "API_URL=http://helloworld:8080/" \
    gateway
```

`helloworld`は名前をつけてコンテナ作成しています。

`gateway`は、まず名前をつけて
外からのアクセスを受け付け、 (`-p "8080:8080"`)
helloworldにlink設定し (`--link="helloworld"`)
環境変数 `API_URL`に 接続先のURLを設定 ( `-e "API_URL=http://helloworld:8080/"`)をしています。

これで `http://$(docker-machine ip dev):8080`にアクセスすると`Hello world` を返すようになります。

このような感じでひとつひとつのサービスを紐付けていきます。

うまい具合に設計するために、などを眺めながら
どのサービスに責任を持つかみたいなことを考えながら行っていったりします。

また上の例はhttpですが、gRPCを使ったり、プロトコルをどうするかも考えて設計することになったりします。

### 参考

* [Microservice architecture patterns and best practices](http://microservices.io/)
* [新しいRPCフレームワーク、gRPCをGoで試してみる](http://www.apps-gcp.com/grpc-go/)

# まとめ

あんまりGo関係なくなってしまいましたが、ここからコンテナの管理のために[Kubernetes](http://kubernetes.io/)や[Amazon EC2 Container Service](https://aws.amazon.com/jp/ecs/)を使うことになったり、[Docker Regstry](https://github.com/docker/docker-registry)を使ってイメージを管理したりしながら、マイクロサービスを作ったりまとめたりしながらひとつのサービスを作っていくことになっていくのかと思います。

Goのbuildは、クロスコンパイルができて、1つの実行ファイルになるので、余計なところでハマらずに済むのが本当に良いなっていうのが最近のGoを使っていての感想です。