コートレビューされると毎回マサカリが飛んできて自分はやはり糞コード製造機だなぁと改めて実感しててまた死にたい感じになってます。mix3です。
golangのWAF調査の一環として
goによる社内アプリ(?)を後輩らと共同でマサカリを一身に受けて血反吐吐きながら書いていたのですが
Revel使っていたけど使いこなせてない感じが半端無くて(Revelのセンスが悪いのか自分たちのセンスが悪いのか考えると多分自分たちのセンスが悪いんだろうなと思う)
あらかた動く様になった段階で、最終的にリライトするのも辞さない覚悟でWAF調査から再出発することに。
とググって見つかったWAFを担当振ってサンプルアプリ作ってどんなもんか調べましょうとなりました。
自分はtrafficを担当。
ただ調べるだけだとつまらないので、Travisやheroku使った事無かったというのもあり、タイトルにあるCI連携とherokuへのデプロイまでやることにしました。
作ったもの
最初はTodoMVCのAngularJSを使うかと考えたがBowerが絡んできて余計な事で悩みそうだったのでやめて、AngularJSで作るToDoアプリを見てシコシコとhtml/js/cssを書いてからtrafficを書いてみた
最終的に出来たのが以下
CI連携
最初にTravisを試して、werckerなるものを後で見つけて追加して、drone.ioなるものもあってそれも追加して、と手当たり次第試してみた
基本的にCIは専用のyml(drone.ioはいらないみたい)をコミットして go test ./... を走らせるだけだけど、genmaiのTravisのテストが賢くてナルホドなってなった
https://github.com/naoina/genmai/blob/master/genmai_test.go#L110
func testDB(dsn ...string) (*DB, error) {
switch os.Getenv("DB") {
case "mysql":
return New(&MySQLDialect{}, "travis@/genmai_test")
case "postgres":
return New(&PostgresDialect{}, "user=postgres dbname=genmai_test sslmode=disable")
default:
var DSN string
switch len(dsn) {
case 0:
DSN = ":memory:"
case 1:
DSN = dsn[0]
default:
panic(fmt.Errorf("too many arguments"))
}
return New(&SQLite3Dialect{}, DSN)
}
}
https://github.com/naoina/genmai/blob/master/.travis.yml
language: go
go:
- 1.2
- 1.2.1
- tip
install:
- go get -v github.com/mattn/go-sqlite3
- go get -v github.com/go-sql-driver/mysql
- go get -v github.com/lib/pq
- go get -v github.com/naoina/genmai
env:
- DB=sqlite3
- DB=mysql
- DB=postgres
before_script:
- sh -c "if [ '$DB' = 'postgres' ]; then psql -c 'DROP DATABASE IF EXISTS genmai_test;' -U postgres; fi"
- sh -c "if [ '$DB' = 'postgres' ]; then psql -c 'CREATE DATABASE genmai_test;' -U postgres; fi"
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE IF NOT EXISTS genmai_test;'; fi"
script:
- go test ./...
.travis.ymlでenvを複数指定するとそれぞれの環境変数でテストが走るので、テストの方で環境変数で使うDBを変える様にしておくことでいい感じにDB別のテストが走っていてとても賢い
order byしてないせいでsqlite,mysqlでは通るがpostgresでは通らないみたいなことがあったので、DB限定しない作りのアプリのテストはこのやり方を真似しておくと良いなと思った
設定に環境変数を使うというのはThe Twelve-Factor Appで推奨されてて環境に合わせて柔軟に設定出来るのは確かに良いなぁと思った
heroku
元々herokuまでやるつもりはなかったけど、作ったものを見える状態にするのも大事だなと思ってCIまでは割とサクサク行ったのでheroku使った事無かったし試してみた
最初 git push heroku master しても全然うまくいかず泣きそうになっていたのだが、最終的には
- godepを使わない
- .godirはgithub.com/mix3/go-traffic-sampleじゃなくてgo-traffic-sampleで
- src以下にきちんと配置(最初はシンボリックリンク使って別の場所で開発してた)
- pushが通らなかった一番の理由が多分これ
という感じでやるとなんとかpush出来た
ググったりgithubで「golang heroku」とかで検索掛けたりするとgodep使ってたり、srcをリポジトリに含めたり含めなかったりしてどうするのが良いのかいまいち分からなくて非常に困った(し、今も良くわかっていない)
結局traffic作者のブログ記事How to deploy Go with Traffic on Herokuをそのままやってうまく行ったので今はそれに合わせている。godepとbuildpack-goは大分トラウマになった気がする。
デプロイしてから静的ファイルが配信出来ていないことに気づいてHow to deploy Go with Traffic on Herokuをよく見るとproductionでも静的ファイルをアプリ側で返す様に弄るよう指示されていたので修正した。
が、herokuにも静的ファイルの配信は出来るハズなので、workerをアプリにしてwebをgoremにしてやればいけるんじゃないかと思ったが、portが不定なのでjsonにport記述できないし、goremはコマンドだからherokuの場合にどうしたらいいのかサッパリ分からなかったのでそこまでは出来なかった。
テスト
revelのときはテスト書いてなかったのでテスト書く場合はどうなるのかなということでCRUDを確認する簡単なモデルテストを書いてみたが、時間が絡むテストが非常に書きにくくて困る(し、今も困っている)
DBから取ってきたデータの比較でreflect.DeepEqualを使ってデータをまるまる比較したのだがdatetime型があるとそれが出来なくて困る。time.Now()をモックできないかとググるも良い情報が無くて詰んでいる。
多分標準パッケージのtimeをどうこうすることは出来ないので、別の時間管理用モジュールを作るか見繕ってきてそれをモックするようにするのが良いのかなと思っている。例えばこれとか?https://github.com/101loops/clock
gvm
goの開発環境を綺麗に保つときにgvmを使うと良さそうなことに気づいた
gvm install go1.*.*
gvm use go1.*.*
とかして、簡単に好きなバージョンのgoが使えるし、
gvm pkgset use global
とかするとインストールしたgo専用のGOPATHが設定されるし、
gvm pkgset create hoge
gvm pkgset use hoge
とかすると、さらにGOPATHが設定される(GOPATH=/path/to/hoge:/pagh/to/global になるのでhoge優先)
ので、適当にgvm installして適当にgvm pkgset create hogeしてglobal以下で開発、依存パッケージをhogeに入れるようにすると環境を何も汚さない感じで開発出来てよいと思う
ひとつ残念なのはgvmは[GVM](the Groovy enVironment Manager)とコマンド名がバッティングしていてそれだけは勘弁してほしいなってなってる
なお
この記事は5/13に書いたのだが、いつのまにか6/15である 時間が経つのは早い
人生の迷子の行き着く先は富士の樹海ではないかと最近良く考えています。mix3です。
p5-IkuSan
IRCボットいい加減今のプロジェクトに欲しくなったので、自分がIRCボット作ったらこうするなというのを以前書いたのだが、今回は実際にそれを作ったという話です。
ただ単に作るだけだと別にUnazuSanで良いじゃないってなっちゃうので、以下の機能を盛り込みました
Getopt::Longの引数が取れる
以前の記事参照
並列にタスク処理が出来る
最初Gearman使って頑張ってたのだけれど @karupanerura さん作、AnyEvent::ForkManagerというのがあるのを見つたのでそれを使うように。worker数を指定出来る様にして並列に処理出来る様にしました
あんまり気にしなくても良いとは思うけれども処理中だからってボットが無反応になるのはなんとなく嫌だったので
タスク処理中、dieしたら共通処理出来る様に
Try::Tinyを使って囲ってるだけなので別にいらないと言えばいらない(自前でtry catchするのでも良い)が、どうせボットが返すエラー文言なんて一律で同じように返したって全然良いと思うので、共通処理を設定出来る様にしておきました
ということで
p5-IkuSanが出来ました
一応CPANに上げるつもりだったので、今まで使ったことなかったMinillaを使ってみたり、plenvとCartonを経由してdaemontoolsで起動出来るようにするというplackアプリの起動で良くある感じのアレをちゃんとやってみたり、その際にCarton経由しつつsystem perl使おうとするとCartonの方で環境変数PERL5OPTが設定されてしまうのでunsetするなりしないと読むモジュールが違って悲しい思いをするということを知ったり、perl5.19.10とか使うとNet::SSLeayあたりでpanicとかなんとか言われて動かなかったり(原因不明)forkしてる関係で子プロセスからsend_messageやらなんやらしようとして死にまくったりして、ikachanリスペクトで頑張ってみたり、衣玖さんはやっぱり可愛いんだなと再認識したりしました。CPANの方にもちゃんと画像出る様にしたい。
あと以下の発言が実現出来てワタクシ非常に満足しております。煩悩はチカラなり。
(Ikusanっていう名前の入ったcpanモジュール作りたいだけです)
— mix3 (@mix3) March 2, 2014
絶賛人生の迷子中、mix3です
golangでWebアプリ
を作ってレビューしてもらったらそれはもうフルボッコでそれこそ雄山のあれな感じでした
isucon3を元に手探りでやったため突っ込みどころを残してしまったようです
- panic使ってerror潰すのやめろ
- パス解決にCallerいらねーだろ
- 一応カレントディレクトリを判断したいという意図だったけどいらないらしい
- 動かし方 go run hoge.go じゃなくて go build hoge.go; ./hoge ってちゃんと書け
などなど
golangのWebアプリの良いサンプル、一応最初に自分で探してはみたものの良い感じのを見つけられず、後で「campoy/todoが参考になるんじゃない?」と教えてもらいました
最初に知りたかった… ちなみにパス解決の突っ込み
https://gist.github.com/mix3/9430209
mix3 ~/tmp/caller_sample $ go run main.go
2014/03/08 21:56:14 loading config (with caller) file: /private/tmp/caller_sample/config/config.json
2014/03/08 21:56:14 OK
2014/03/08 21:56:14 loading config file: config/config.json
2014/03/08 21:56:14 OK
mix3 ~/tmp $ go run caller_sample/main.go
2014/03/08 21:56:38 loading config (with caller) file: /private/tmp/caller_sample/config/config.json
2014/03/08 21:56:38 OK
2014/03/08 21:56:38 loading config file: config/config.json
2014/03/08 21:56:38 open config/config.json: no such file or directory
exit status 1
こんな感じで実行パスに左右されないパス解決が出来るので使い方間違ってなければ意味はある書き方だとは思うんですが、まあ今回は必要ないよってことですかね
revel使いましょ
そんな感じで センスの無い やつが手探りでやると悲惨なことになるのでWAFに乗っかった方がきっと良いと判断されたっぽい(実はそこらへん曖昧だったり…)のでとりあえずrevelにのっかることにしました
使い方は公式http://robfig.github.io/revel/index.htmlのtutorialとmanualとsample眺めてればなんとなく分かる感じです
以下revel/samples/bookingにはgorpとsqliteを使ったサンプルコードがあるのだけど、「mysqlにするにはどうすれば?」と少し悩んだのでメモ
conf/app.conf のsqliteの設定をmysqlのものに変更
db.import = github.com/go-sql-driver/mysql
db.driver = mysql
db.spec = root@tcp(localhost:3306)/alphawing
app/controllers/gorp.go のimportしているドライバをmysqlのものに変更
_ "github.com/go-sql-driver/mysql"
app/controllers/gorp.go の gorp.DbMap{} を mysqlのものに変更
Dbm = &gorp.DbMap{Db: db.Db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
だいたいこれくらい弄るとmysqlで動く様になるはず
ちなみにsampleだからだと思うのだけど以下のような感じでコントローラ処理の前後にBegin,Commit,Rollback を入れ込んでて吹いた
func init() {
revel.OnAppStart(InitDB)
revel.InterceptMethod((*GorpController).Begin, revel.BEFORE)
revel.InterceptMethod((*GorpController).Commit, revel.AFTER)
revel.InterceptMethod((*GorpController).Rollback, revel.FINALLY)
}
modelsでちゃんとやりたいときにはどうしたらいんだろうか…
おめでとうございます
今年もよろしくお願いします
ちょっと詰まったことがあったのでメモ書き これ書いてる時点でVagrantのバージョンは 1.4.1 となります
Virtualboxのネットワークアダプタにはいくつか種類があり
- NAT
- ゲストOS -> 外
- 内部ネットワーク
- ゲストOS <-> ゲストOS
- ホストオンリーアダプタ
- ホスト -> ゲストOS
あたりを組み合わせて遊んだりするのですが、Vagrant で内部ネットワークを設定する方法が分かりづらかったのでメモ
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "box_name"
config.vm.define :host1 do |host1|
host1.vm.hostname = "host1"
host1.vm.network :private_network, ip: "192.168.1.111", virtualbox__intnet: "intnet"
end
config.vm.define :host2 do |host2|
host2.vm.hostname = "host2"
host2.vm.network :private_network, ip: "192.168.1.112", virtualbox__intnet: "intnet"
end
config.vm.provision "shell", inline: "/etc/init.d/network restart"
end
抜粋するとこんな感じ
Private Networks - Networking - Vagrant Documentation とか見てると
virtualbox__intnet: true
とか書いてるけどそのまま設定すると「(trueが)文字列じゃねーよゴルァ!」と怒られるので、何かしら文字列を指定してあげる。
Virtualboxで設定した場合、内部ネットワークを指定した時のデフォルトが intnet なのでintnetで良いでしょう。
で、これで内部ネットワークの設定が出来るのですが初回の vagrant up 直後はNATによる設定で外に行けるはずなのに繋がらなかったりするので、
Shell Provisionerでnetworkの再起動をしてます。もっとスマートなやりかたあればいいんだけど。