この記事は Go その2 Advent Calendar 2015 10日目の記事です。
今回はgoとdockerを使ってMicroserviceっぽく、ウェブアプリケーションを作っていく時のさわりの部分を書いてみました。
Microservice Hello World
macでdocker toolboxインストールして、docker-machine start dev
とdocker-machineを立ち上げた後から。
Hello World
を返すウェブサーバーのdockerイメージを用意してみます。
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)
}
あらかじめビルド。
$ GOOS=linux GOARCH=amd64 go build helloworld.go
Dockerfileはこんな感じ。
FROM busybox
COPY ./helloworld /helloworld
CMD ["/helloworld"]
docker build。
$ docker build -t helloworld .
あとはイメージを実行するだけです。
$ docker run -p "8080:8080" --rm helloworld
$ curl http://$(docker-machine ip dev):8080
Hello World
dockerイメージの容量を小さくする
dockerイメージはできるだけ小さくするようにするのが一つのプラクティスのようです。
上で使っているbusyboxは最小のパッケージですが、単純にgoを実行するだけならばこれで十分です。
dockerを使った運用を行う際に https://github.com/docker/distribution などに
イメージを登録したり、イメージを引っ張ってきたりするのですが、容量が大きいとそのぶん時間がかかります。
RUNなどをワンライナーで書いているのもビルドを早くしたり、容量を下げたりする方法のひとつです。
RUN echo '#!/bin/sh' > /usr/sbin/policy-rc.d \
&& echo 'exit 101' >> /usr/sbin/policy-rc.d \
&& chmod +x /usr/sbin/policy-rc.d \
少し油断すると、すぐに容量が肥大化するので注意しておくことのひとつになります。
環境ごとに動作を変える
開発環境・ステージング環境・本番環境を用意する場合、その環境ごとにコンフィグの設定を用意して、起動時に引数などに設定して実行することがあります。
Railsの場合なら rails server -e production
と環境を指定し、
config/environments/<ENV>.rb
ファイルを読んで環境ごとに設定を変更する、といったところです。
これは便利ではあるのですが、設定ファイルのルールをきちんと決めておかないと、各所に散乱してしまったり、設定の仕方がフレームワークに依存してしまったり、リポジトリ内に外部に公開できない情報が含まれてしまうという問題もあったりします。
特にマイクロサービスの場合、小さな機能を持つサービスをいくつも立ち上げることが前提です。設定ファイルに依存していると、まず元のリポジトリにファイルを追加をしてから、そのイメージをビルドして、デプロイ作業を何度もやらなくてはいけなくなります。
development
がもうひとつ必要になったときに development002
といった環境名を逐一つけなければいけないのも大変です。
そしてそのビルドされたイメージの設定がどうなっているかを知る術が欲しくなったりします。Goの場合は、ビルドした実行ファイルを置くだけで良かったのが環境ファイルもイメージに入れて実行時に参照など最初の理想から離れてしまいます。
そんな経緯があるのか http://12factor.net/config では、設定は環境変数に入れる方法を取り上げています。
サービス間の連携をする
マイクロサービスなので、サービス間で通信することになります。
例えば上で作成したhelloworldにアクセスするのに、
ひとつ間に挟む gateway というコンテナを挟んでみます。
FROM busybox
COPY ./gateway /gateway
CMD ["/gateway"]
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イメージの中に入れます。
そしたら、各イメージのコンテナを作成します。
$ 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を使ったり、プロトコルをどうするかも考えて設計することになったりします。
参考
まとめ
あんまりGo関係なくなってしまいましたが、ここからコンテナの管理のためにKubernetesやAmazon EC2 Container Serviceを使うことになったり、Docker Regstryを使ってイメージを管理したりしながら、マイクロサービスを作ったりまとめたりしながらひとつのサービスを作っていくことになっていくのかと思います。
Goのbuildは、クロスコンパイルができて、1つの実行ファイルになるので、余計なところでハマらずに済むのが本当に良いなっていうのが最近のGoを使っていての感想です。