この記事はCircleCI Advent Calendar 2015の5日目の記事です。
はじめに
Go+Web+CIの実戦的な話です。Go+CIに関してだと色々な手法は出てきていますが「実際どうすればいいの?」と感じている方が多いと思います。
また、昨今Go+Webアプリも増えているので、これからGoでWebアプリケーションを作ろうと思っていた方の継続的インテグレーションへのアプローチの足掛かりとなれば嬉しいです。
(Go要素強いです)
_人人人人人人人人人人人人人人人人人人人人人人人人人人人_
> 本当は12/6のGoConferenceで話そうと思ってたんだぜ!! <
 ̄YYYYYYYYYYYYYYYYYYYYYYYYYYY ̄
※ GoConのセッション外れた+抽選も外れた勢
さて、今回使用したリポジトリは下記にあります。
Base64 Encode/Decodeするだけですが、Go+Web+CIサンプルとしても使えるので是非。
やっていること
circle.ymlに沿ってすすめるのが難しいので、やっていることベースで話を進めたいと思います。circle.ymlはあっちいったりこっちいったりしますので、ご了承ください。
- Goインストール
- goxを用いてCross-Compileし、ghrを用いてGitHubへリリース
- 実行ファイルにビルドバージョンとビルド日時をセットする
- 静的解析:
gofmt
,go vet
,golint
- Request Collection Runner (広く呼ばれる名前を知らない…)
◆ Goインストール
yaml
machine:
pre:
- >
curl -o go.tar.gz -sL https://storage.googleapis.com/golang/go1.5.2.linux-amd64.tar.gz &&
sudo rm -rf /usr/local/go &&
sudo tar -C /usr/local -xzf go.tar.gz &&
sudo chmod a+w /usr/local/go/src/
自分でGoをインストールしています。ただ、それだけです。
moovweb/gvmを使うのもアリだと思います。が、ちゃちゃっとやりたいときや、pre-releaseのものを使用したい場合が出てくる可能性もあるので、自分でインストールしてしまう方が速いです。Goに限って言えばGoのインストールは超楽なので。
- Go公式が配布しているパッケージはこちらにあります。
◆ goxを用いてCross-Compileし、ghrを用いてGitHubへリリース
これはGo+CIなら有名だと思います。が、一応紹介しようと思います。
- mitchellh/gox -
Goは基本的に稼働しているOSに対応する実行ファイルを作成しますが、ターゲットを指定するとクロスコンパイルすることが可能です。
クロスコンパイルは非常に簡単ですが、知らないと手こずるのでgoxを使用するのが吉かもしれません。
- tcnksm/ghr -
ghrは高速に自作パッケージをGithubにリリースするghrというツールをつくったが作者ページなのでこちらを一読ください。
yaml
deployment:
release:
branch: master
commands:
- go get github.com/mitchellh/gox
- go get github.com/tcnksm/ghr
- gox -ldflags "-X main.BuildVersion $BUILD_VERSION -X main.BuildDate $BUILD_DATE" -output "dist/${CIRCLE_PROJECT_REPONAME}_{{.OS}}_{{.Arch}}"
- ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags` dist/
使用する gox
と ghr
を go get
し、まずは gox
でビルドします。ビルドして作成される実行ファイルは dist
ディレクトリに格納しています。
$CIRCLE_PROJECT_USERNAME, $CIRCLE_PROJECT_REPONAME ?
CircleCIでは、コンテナを起動する際(Start container
)にリポジトリとユーザの情報を環境変数に登録してくれます。
時折、いや、結構いる気がするのですが、このCircleCIが設定してくれる環境変数を知らない方がいます。
これないとマジックナンバーばかりのcircle.ymlとなってしまう可能性もあるので気をつけて下さい。
Tips: CIRCLE_COMPARE_URL
と git log --name-only
を使って差分ファイルを見つける
弊社のプロジェクトの場合、CIRCLE_COMPARE_URL
を利用してハッシュ部分を取り出し、前回と今回で変更点があったファイルに関係するテストを走らせることをしています。
例えば、git log --no-merges --name-only 9bb7a0a346ac...d32dc33ead2c
のようにして結果を取得し、grep
して修正ファイルを見つけ出して、限定したテストを走らせることによってCircleCIを効率良く回すことをしています。
また、CIRCLE_BRANCH
がmaster
だった場合はフルテストを行なうこともしています。
話を戻しましょう。
次に ghr
で dist
に格納した実行ファイルをGitHubへリリースします。リリースするためには
-
- GitHubの Personal access tokens へ遷移する
-
-
Generate new token
でトークンを作成する。(スコープは変更しなくてよい)
-
-
- 作成されたトークンをコピーする
-
- CircleCIの Project Settings > Tweaks > Environment variables へ遷移する
-
- Nameを
GITHUB_TOKEN
にし、Valueに"3"でコピーしたトークンをペーストする
- Nameを
注意:面倒だからといって、GITHUB_TOKEN
はcircle.ymlに直書きしないよう気をつけて下さい。
ここまですればcircle.ymlはコピペでOKです。
実行後にリリースページを参照すると
のようになっています。
もし、実行ファイルがアップロードできていない場合はリリースタグを適当にひとつ設定してください。
◆ 実行ファイルにビルドバージョンとビルド日時をセットする
あるエンドポイントを叩くと、実行ファイルのバージョン(コミットハッシュ)とビルドした日時をわかるようにしています。Webアプリケーションを作成するなら絶対にあった方が良いです。
$ curl -XGET "localhost:8080/version"
Version: d32dc33-dirty
Date: 2015-12-05T07:50:13+0000
このバージョン情報があるだけで、前回との差分を容易に調べることができます。
hash=`curl -XGET "localhost:8080/version" | (sedなどで出力を整形する) `;
git --git-dir=$GOPATH/src/github.com/kaneshin/base64server/.git log \
--no-merges --oneline ${hash}...origin/master
これでリリースするコミットを簡単に得ることができます。弊社のプロジェクトではJSONフォーマットなので、出力の整形はそれに応じた形で対応しています。
さて、これの実装方法 (circle.yml) です。
yaml
machine:
environment:
CHECKOUT_PATH: $HOME/$CIRCLE_PROJECT_REPONAME
post:
- >
echo "export BUILD_VERSION=\"`git --git-dir=${CHECKOUT_PATH}/.git describe --always --dirty`\"" >> ~/.circlerc;
echo "export BUILD_DATE=\"`date +%FT%T%z`\"" >> ~/.circlerc;
deployment:
release:
branch: master
commands:
- go get github.com/mitchellh/gox
- go get github.com/tcnksm/ghr
- gox -ldflags "-X main.BuildVersion $BUILD_VERSION -X main.BuildDate $BUILD_DATE" -output "dist/${CIRCLE_PROJECT_REPONAME}_{{.OS}}_{{.Arch}}"
- ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags` dist/
先ほどの gox
の部分でスルーしたのですが、 gox
でビルドする際に ldflags
オプションをつけています。
これは go build
のオプションで、-X
で指定した変数(今回だとmainパッケージのBuildVersion
と BuildDate
)に情報を設定することができます。
それぞれ、
-
BuildVersion
には$BUILD_VERSION
-
BuoldDate
には$BUILD_DATE
を付与しており、それらの $BUILD_{VERSION,DATE}
はmachine
セクションで定義しています。
echo "export BUILD_VERSION=\"`git --git-dir=${CHECKOUT_PATH}/.git describe --always --dirty`\"" >> ~/.circlerc;
echo "export BUILD_DATE=\"`date +%FT%T%z`\"" >> ~/.circlerc;
~/.circlerc
CircleCIでは変数をexport
しても次のステップでは無力化されています。
ですが、~/.circlerc
というファイルが毎度のステップで読み込みをしてくれるため、ここでexportを定義すれば machine.environment
と同じ効果を得ることができます。
◆ 静的解析: gofmt
, go vet
, golint
Goの静的解析で失敗すればエラーとなるようにします。これで綺麗なプロジェクトを保つことができます。
静的解析には
- gofmt
- go vet
- golint
- goveralls
が有名でしょう。今回、goverallsは省きますが、絶対にやったほうがいいです。カバレッジはテスト駆動開発のひとつの指標となるためです。
(カバレッジはテストを書くモチベの源泉である - 参考:XcodeCoverageについて)
yaml
test:
override:
- test -z "$(gofmt -s -l . | tee /dev/stderr)"
- go vet ./...
- test -z "$(golint ./... | tee /dev/stderr)"
- go test -race -test.v ./...
gofmt
test -z "$(gofmt -s -l . | tee /dev/stderr)"
gofmtを行い、フォーマットされていないファイル名を出力します。test -z
で文字があるなら false
が返却されて、CircleCIが失敗となります。
go vet
go vet ./...
struct
につけるタグ(アノテーション)を解析してくれます。
go build
ではスルーされてしまうので、仮に違反したタグが存在したとしてもそのままビルドが完了し、期待した結果が返ってこなくてめっちゃハマるでしょうね…(アノテーションでハマる人はたくさんいる)
golint
test -z "$(golint ./... | tee /dev/stderr)"
GoのLintです。コメント等なければ下記のようにその旨を返却してきます。
main.go:13:2: exported var BuildVersion should have comment or be unexported
エクスポータブルな変数にコメントがないと絶対エラーになるので、コメントの強制力がハンパないです。
◆ Request Collection Runner (広く呼ばれる名前を知らない…)
yaml
test:
pre:
- go version
- go build -a -v -o /tmp/base64server .
- /tmp/base64server -port=${APP_PORT}:
background: true
override:
- ./runner.sh
実際に実行ファイルを起動して、意図しているリクエストをサーバが受け付けているかを確認します。
一般的な名前がわからないのですが、Postmanにある機能名をパクりました
runner.sh
curl --fail
で叩いて、exitコードを見ているだけです。
#!/bin/bash
if [[ -z "${APP_PORT}" ]]; then
APP_PORT="8080"
fi
function __check()
{
req="localhost:${APP_PORT}/${1}";
curl --fail -XGET "${req}"
ret=$?
if [[ ! $ret = "0" ]]; then
echo "ERROR: ${req}" > /dev/stderr
exit $ret
fi
}
# version
__check "version"
# encode
__check "encode?v=hello+world";
# decode
__check "decode?v=aGVsbG8gd29ybGQ=";
APIの実装とかでroutingをぶっ壊す人が稀にいますからね。(弊社にはいません)
おわりに
すごく長くなってしまいました…(こんなはずでは…)
Go+CIでは他に goveralls
も話したかったのですが、なかなか長くなってしまったので今回は省きました。
そして、、12/5の本日、たまたまCircleCIのUIが変わったせいで、スクリーンショットを全て作り直しましたとさ…