Go+Webアプリケーション+CircleCIで静的解析・テスト・バイナリリリースを効率良く行なう

  • 44
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は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/

使用する goxghrgo get し、まずは gox でビルドします。ビルドして作成される実行ファイルは dist ディレクトリに格納しています。

$CIRCLE_PROJECT_USERNAME, $CIRCLE_PROJECT_REPONAME ?

CircleCIでは、コンテナを起動する際(Start container)にリポジトリとユーザの情報を環境変数に登録してくれます。

Screen Shot 2015-12-05 at 6.11.19 PM.png

時折、いや、結構いる気がするのですが、このCircleCIが設定してくれる環境変数を知らない方がいます。
これないとマジックナンバーばかりのcircle.ymlとなってしまう可能性もあるので気をつけて下さい。

Tips: CIRCLE_COMPARE_URLgit log --name-only を使って差分ファイルを見つける

弊社のプロジェクトの場合、CIRCLE_COMPARE_URLを利用してハッシュ部分を取り出し、前回と今回で変更点があったファイルに関係するテストを走らせることをしています。

例えば、git log --no-merges --name-only 9bb7a0a346ac...d32dc33ead2cのようにして結果を取得し、grepして修正ファイルを見つけ出して、限定したテストを走らせることによってCircleCIを効率良く回すことをしています。

また、CIRCLE_BRANCHmasterだった場合はフルテストを行なうこともしています。


話を戻しましょう。

次に ghrdist に格納した実行ファイルをGitHubへリリースします。リリースするためには

  • 1. GitHubの Personal access tokens へ遷移する
  • 2. Generate new token でトークンを作成する。(スコープは変更しなくてよい)
  • 3. 作成されたトークンをコピーする
  • 4. CircleCIの Project Settings > Tweaks > Environment variables へ遷移する
  • 5. Nameを GITHUB_TOKEN にし、Valueに"3"でコピーしたトークンをペーストする

注意:面倒だからといって、GITHUB_TOKENはcircle.ymlに直書きしないよう気をつけて下さい。

Screen_Shot_2015-12-05_at_6_10_23_PM.png

ここまですればcircle.ymlはコピペでOKです。

実行後にリリースページを参照すると

Screen Shot 2015-12-05 at 6.13.25 PM.png

のようになっています。

もし、実行ファイルがアップロードできていない場合はリリースタグを適当にひとつ設定してください。

◆ 実行ファイルにビルドバージョンとビルド日時をセットする

あるエンドポイントを叩くと、実行ファイルのバージョン(コミットハッシュ)とビルドした日時をわかるようにしています。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パッケージのBuildVersionBuildDate)に情報を設定することができます。

それぞれ、

  • 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しても次のステップでは無力化されています。
- 詳細:https://circleci.com/docs/environment-variables#custom

ですが、~/.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が変わったせいで、スクリーンショットを全て作り直しましたとさ…