前提: GitHub flow を使っていてCIサーバーはJenkins
最近ちょっと開発フローの改善をして、とてもよく機能してて満足しているので紹介してみる。
この改善をやる前の悩み:
- pull-requestでコードレビューはできるのだけど、cssとかjavascriptなどの見た目や動作の変更ってコードだけだとわかりにくい。レビューする人が各自ローカル環境で実行するのもだるい。
- コードを読まないデザイナーとかプロダクトオーナーとかの人が、pull-requestのレビュープロセスに簡単に参加できない(非開発者全員のところでローカル環境設定するのはだるすぎる)。
- コード的にokに見えてmasterにmerge後、何か問題(特に仕様的な問題や、デザイン的な問題)が発生した場合、「修正branchを作ってpull-request」というフローを再度回さないといけない。最初のpull-requestで非開発者の人を巻き込めたらそこで気付けるのに。
これらの問題を解決するのに、feature branchもHerokuにJenkinsから自動的にデプロイするようにしたら色々幸せになれた。
具体的には、N個(N=アクティブな開発者の数くらいが良さそう)のfeature branchのテスト用のHeroku appを予め作っておいて、feature branchにpushされるたびに、Jenkinsからその中の一つへデプロイされる感じにした。
補足:
- 同じfeature branchの新しいpushがなるべく同じHeroku appにデプロイされ続けるようにとか、heroku configもするとか、多少工夫してる。
- 初期の頃、feature branch毎にJenkinsから毎回heroku createしてたが、それはさすがに怒られそうなので、複数をローテーションで使うようにした。
- いまどき?だとJenkinsで動的に仮想環境とか作っちゃうのもありだと思う。でも、Herokuが楽過ぎて頑張って仮想環境作る必要もないかな、というのが現在の弊社の状況。
非開発者にもレビューして欲しいときは、pull-request上でmentionして、heroku上のデプロイされたものを見てもらう感じ。基本的にUIに変更のあるようなものは非開発者もチームほぼ全員見るようにしている。
数ヶ月運用してるけど、もうこれなしでは生きるのが辛い感じにまでなってきてる。
非開発者の人もとても楽になった感じで、それまでは、色々な変更が入った(または肝心なものがコミュニケーションの問題で入っていない)masterブランチが動作しているのを見るしかなく「何が」実際変更されたのかわかりづらかったが、このフローにしてから、変更点がわかりやすく開発者とのコミュニケーションもだいぶ楽になった。また、チームメンバーの誰かが「思ってもみなかった」変更がいきなり入ってしまうということもなく、だいぶストレスが減った感じ。
また、開発者も、それまでは、merge後、非開発者に指摘されると「めんどくせーなー」という意識が生まれがちで、かつ、その場合mergeから時間が経っていることも多く、その変更に対するフレッシュさが減り、再度作業を始めるまでの心理的な負担(実際、実装を思い出すための脳のコストも高そう)も高かった。それが、pull-request中に非開発者からフィードバックが貰えることで、そのpull-requestにcommitを追加することだけでさくっと修正できて、全体のスピード感がとてもあがったし、開発者の負担も減った。
つまり、pull-requestのmergedがチームメンバー全員にとってのDONEというステータスになり「実装終わったけどまだ本番には入っていない」とか「チームメンバーが納得してないものが本番に入っちゃった」みたいな事故/ストレスを減らせた。Done means DONE!
これらの結果、masterにmerge後、Jenkinsからの自動デプロイもかなり安心してできるようになった。
おまけ:
@hsbtさんが いい感じのGitHub flowベースのワークフローをあげていたので、弊社(Quipper)との差分を書いてみる。
- masterブランチは常にデプロイ可能な状態でかつ自動デプロイされている。
- なので、手でデプロイという作業は発生しない。
- WIPのpull-requestはそれほど推奨してない。この辺はpull-requestの粒度の話もあるので別エントリで書きたい感じ。
- 関連して、チェックリストみたいのも基本作らない(必要ない)。
- mergeはレビューした人が行う。
- コードレビュー以外のテストも上に書いたように、feature branchでしてしまう。
- インフラメンバーいない。(が、そろそろ欲しい! 興味ある人ご一報を)。
- Release’s notes 作成してない。
フローの中でGithub(とJenkins)の外へ出たくない、という方針。
この辺りって、絶対的に正しい方法というのはなく、組織のサイズとかサービスの規模に応じて、ダイナミックに変えていくのが正しい道だと思っている。
Quipper、日本オフィスができて半年以上達ち、このブログでも改めて色々発信してみようと思ってはいるのだけど、一度間が空いてしまったブログの再開はなかなか難しい(本人以外誰も気にしていない現実を知りつつ)。この状況を打破するために、軽いのをまず書いてみる。本当はQuipperの開発について色々書きたいんだけど、それはまた次回。
最近出会った mailtrap.io というサービスがWebシステム開発にとてもいい感じなので紹介してみる。
メール送信は、ある程度テストを自動化したとしても、繰り返し、手で実行して目で確認することも多い。テストするときは、送信先アドレスを自分にして、送信して、自分のメールボックスを開いて確認する、とか。めんどくさい。何か問題を発見したら、関係者にメールをフォワード、とかもめんどくさい。ステージング環境では実際に送らずに、ログに出すという方法もあるけど、これだと、開発者ではない人には使いにくい。開発者であっても、何か問題があったら、それをコピペして、issue管理システムに貼る、とかめんどくさい上にレイアウトも崩れがち。
この mailtrap.io というサービスでは、これらのめんどくささからある程度解放してくれる。このサービスは "Fake SMTP server for development teams to test" と書いてあるまんまで、SMTPサーバのように動くのだけど、実際にはメールを送信せずにそのサービスに溜まっていき、送ったメールの内容をWeb UIから見ることができるサービス。
Quipperでも、ステージングサーバーに入れて使っている。実際に使ってみると使い勝手がかなり良くて、
- 一通一通にpermanent linkができるので、issue管理システムとかチャットとかで、共有しやすい。
- HTMLメールとかも普通に見える。
- 貯めてある状態からワンクリックでメールを実際に送信することもできる。「実際のメールソフトで見てみたい」というときにも便利に使える。
- どんな 宛先(to)でも送ることができるので、テスト環境で作った適当なデータでもちゃんとメールの送信のテストができる。
- UIがとても簡単。Gmailみたいな感じで誰にでも簡単に使える。ステージング環境でテストするのは開発者だけではないので、誰でも簡単に使えるというのはとても重要。
などなど。
設定方法は、単純にSMTPの口が提供されるだけなので、それを設定するだけ。Quipperでは、Railsアプリには config/initializers/mailtrap.rb
みたいのを置いている。 (herokuを主に使っているので、設定はENV経由)
if ENV['MAILTRAP_HOST'].present?
ActionMailer::Base.smtp_settings = {
address: ENV['MAILTRAP_HOST'],
authentication: :plain,
enable_starttls_auto: true,
password: ENV['MAILTRAP_PASSWORD'],
port: ENV['MAILTRAP_PORT'],
user_name: ENV['MAILTRAP_USER_NAME'],
}
Mail.defaults do
delivery_method :smtp, ActionMailer::Base.smtp_settings
end
end
このサービス、今のところ無料。「メインのビジネスじゃないから」らしい。
Wikiというものはとても便利なんだけど、
- 大量の文章を書くにはWebブラウザのインターフェースはまだまだ辛い
- オフラインで使えない(文章書くのは電車が一番)
- 複数の文章を再構成したり、一括で検索したり、置換したりは、Webだとやっぱりきびしい
と言った欠点がある。
とは言え、誰でも気軽に編集できるWikiの魅力も捨てがたい。
そこで、「Wikiではあるんだけど、ローカルでも自分の好きなエディタで簡単に編集できるツールないかなー」と探してみたら、 Gitit というWikiを発見した。
ここ数日、結構な量のドキュメントをGititで書いてみて、わりと満足しているのだけど、検索してもGititの日本語の情報があまり出てこないので紹介してみる。
Gititの特徴
- コンテンツをGitのレポジトリに保存する。
- そのGItレポジトリをcloneして好きなようにいじってからcommit/pushすれば即座にWikiに反映される。
- Wikiとしてはとても普通。マークアップはmarkdownがデフォルトだけど他にも選べる。各種言語のコードを綺麗に色付きでフォーマットしてくれる。
- Haskell製でpluginとかもHaskellで書く。
- Webサーバを内蔵している。
- 独自のメタ情報のようなものを持たずに、Gitレポジトリ内のディレクトリ/ファイル構成がそのままWikiになる。ファイル名がそのままWiki名になる。もしやめたくなった場合に他のツールへの移行も楽。
- テキスト以外の画像とかもそのままGitレポジトリに入る。もちろん、cloneしたレポジトリに画像を入れてcommit/pushすればアップロードされる。
- Gitそのままなので分散Wikiみたいのも簡単に作れるし、branchを使ってのドキュメントの管理とかもできる。
Github製の gollum と似ているが、Gititの方が良さそう。
デモサイトはこちら。 Wikiとしては、普通中の普通なので面白くないけど。
Haskell製と知って、インストールめんどくさそうだなーと思ったんだけど簡単だった。
簡単な導入手順 – Ubuntu編
$ sudo apt-get install ghc $ sudo apt-get install cabal-install
$ cabal update $ cabal install pandoc gitit -fhighlighting --reinstall (pandocとgititを同時にinstallしないとhightlightが有効にならなかった)
path に ${HOME}/.cabal/bin を追加
$ mkdir ~/hogehoge $ cd ~/hogehoge $ gitit --print-default-config > gitit.conf $ <editor> gitit.conf $ gitit -f gitit.conf
これで、port 5001で起動するので、 http://localhost:5001/ とかで。外に公開するなら、apacheとかnginxでreverse proxyすればいいだろう。
コンテンツを直接いじりたい場合は、
git clone ssh://example.com:~/hogehoge/wikidata
とかで、普通にcloneして後はお好きなエディタでファイルを編集して、commit/push すればその場で反映される。
こんな感じで、Gititを使うと、gitを使ったプログラミングと同じフロー/リズムでドキュメントを書けるところが気に入ったのでした。
オススメ。
ここ数ヶ月、Google App Engine/Pythonを使い、初めてちょっとしたものを作ってみているのだけど、開発初期から知っておけばよかったなー、と思うノウハウ/tips的なものをずらずらと書いてみる。
基本的な環境設定は、 以前書いた まま。
0. 公式ドキュメントを良く読む
言うまでもなく、だけど、 マニュアル はもちろん、 この辺 の下の読み物も、流し読みだけでもしておいたほうがいい。
datastoreとmodel的なところ
1. key nameを使いこなす
key nameは、レコードの作成時に指定できる(RDBでいう)primary keyの別名みたいなもの。primary key自体は自動的で作成されるので開発者が指定できるのはkey nameだけ。
key nameをうまく使うことで、datastoreを使いやすくすることができる。特にdatastore上で"unique"を表現したいときに活躍する。
たとえば、 Modelクラスのget_or_insert では、key nameを使うことで簡単にuniqueなレコードを作成することができる。get_or_insert()と言う名前通り、 key nameで指定したレコードが存在したらget、しなかったらそのままinsertといういうことができる。この処理はatomicに行われるので、同じkey nameのレコードが複数存在してしまうこともない。
get_or_insert()の注意点としては、 get時には渡した値ではupdateされない、ということにということがある。そのまんまget or inssert。
2. どういうkey nameを使うべきか
- 他システムから取り込んだレコードでuniqueとされている値
例えば、twitter botとか作るなら(作ってないけど)、twitterのユーザIDとか、tweet毎のIDとかそのまま key nameに入れてしまうのが良さそう。
今作っているシステムでは、別システムのMongoDB上で作ったデータをそのまま取り込んでいる部分がある。そういうデータは MongoDBのBSON ID を文字列にしたのをそのまま key nameにしている。なかなかいい感じ。
- クライアント側で生成されたデータ
クライアント側で生成されるようなデータをdatastoreに格納する場合、クライアント側でUUIDみたいのを作成し、それをそのままkey nameにするのもなかなかよさそう。
このUUIDをkey nameにしてget_or_insert()することで、クライアント端末やネットワークの不調等で同一データが2回POSTされたりしても、データが二重になったりしないのがなかなか気に入っている。もちろんUUIDを偽造されても問題ないような作りにしておく必要はあるけど。
- 複合キー
key nameは一つしか設定できないのだけど、ただの文字列なので上記の値を結合して関連テーブルを作成したりできる。これは場合によってはなかなか便利。
- その他
uniqueっぽいものがないモデルの場合でも、とりあえずランダムで長めの文字列を指定しておくとよいかも。後からは指定できない。
3. GQLで key nameで検索
key nameでのオブジェクトの取得は、基本的には、 get_by_key_name を使えばいいのだけど、WebのadminコンソールとかGQLしか使えないようなところでは、こんな感じに書ける。
WHERE __key__ = KEY('FooModel', 'cff0611529520910195ed357dd69f908')
4. get_or_insert() でgetされたかinsertされたかを知る
get_or_insert() を実行した結果、getされたかinsertされたかがそのままでは簡単にはわからないのが、これを知りたいときがある。例えば「get_or_insert()してgetだった場合には〜する」みたいなとき。
こういうときのために、次のようなclass methodを作っている。
@classmethod
def get_or_insert2(cls, key_name, **kwds):
def txn():
entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
if entity is None:
entity = cls(key_name=key_name, **kwds)
entity.put()
return (True,entity)
return (False,entity)
return run_in_transaction(txn)
または、 insertに成功した場合だけオブジェクトを返し、既に存在する場合はNoneを返す処理
@classmethod
def insert_or_fail(cls, key_name, **kwds):
def txn():
entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
if entity is None:
entity = cls(key_name=key_name, **kwds)
entity.put()
return entity
return None
return run_in_transaction(txn)
5. unique制約
datastore上で、unique制約を作るのはちょっと大変。ユーザ情報を格納するUserモデル内でemailアドレスをuniqueにしたい、とか非常にありがちなことが簡単にはできない。
こういう場合にも、key nameがうまく使える。unique制約を保持するためのモデルを別に作って、uniqueにしたいデータ(この場合emailアドレス)をkey nameにしてしまうのがいい感じ。上記の、insert_or_fail で作成してみて、作成できたらそのemailアドレスは使える、という感じ。(何か他に定石がありそうなんだけど。)
6. データの一括更新
システムのアップグレード時など、データを一括で更新したくなるときがある。SQLだとUPDATE一発みたいな更新でも、datastoreだとちょっと大変。こういうときは map/reduce を使うと素早く簡単に全件の更新ができてよい感じ。(件数多いと、CPU時間==お金を一気に消費しちゃうけど)
(このappengine-mapreduceはmap/reduceってか、単に全レコードに対して並列で何かができるだけなんだけど。本当のmap/reduceはそのうち導入されるらしいので楽しみ。)
7. ランキング
ちょっとしたゲームっぽいものを作ると「スコアランキング」みたいなものが必要になったりするものだけど、このランキングってのはRDBでもそれなりに厄介なもので、何も考えないで作ると、大量のアクセスが発生してしまい使いものにならなくなる。その結果、バッチ処理で静的なランキングを作成するとかちょっとダサい感じになってしまいがち。
で、ちょっと自分で色々なアルゴリズムで実装してみたりしてたのだけど、便利なライブラリを見つけてしまったのでそれを使っている。 このライブラリ。
8. 複数レコードの保存/削除は db.put() / db.delete() を使って一括で行う
スピードが全然違う。db.delete() はクエリをそのまま突っ込んだりもできる。
9. 全文検索
全文検索は、現在の Google App Engine ではまともに実装されていないらしい。(Googleのくせに! 近い将来実装されると発表されたが)
ただ、簡易的には SearchableModel が使える。使い方は、 コードの中のコメント が一番詳しいような気がする。
そのコメントにあるように、かなり低機能で日本語も使えない。 ただ、とりあえずそれっぽく動かすだけなら簡単に使える。
ログ関連
10. loggingを使ってログを取る
logging moduleを使うことで、実行時に色々なログを取れる。Google App Engineともうまく統合されていて、Web上のadminインタフェースで、httpアクセス毎に参照できてとても便利。
11. POSTされたデータとそれに対応するresponse bodyをすべてログに
loggingを使ってPOSTの中身とresponseをそのままログに取っておくとデバッグ時に便利。Google App Engineの管理画面からアクセス毎にPOSTとそれに対応するresponseの内容を見ることができる。
これをするためのMiddlewareを書いた。(実際には 前回 書いたようにセッション管理用の SessionMiddleware も挟んでいる。)
class PostLoggerMiddleware(object): def __init__(self, app): self.app = app
def __call__(self, env, start_response): request = webapp.Request(env) if request.body: logging.info("request: " + str(request.body))
def my_start_response(status, response_headers, exc_info=None): write = start_response(status, response_headers, exc_info) def my_write(body): logging.info("response: " + str(body)) write(body) return my_write
return self.app(env, my_start_response)
application = PostLoggerMiddleware(webapp.WSGIApplication([ ...
12. exceptionをtrackback付きでログに
基本的に何もしなければ、Traceback付きでログに乗る。しかし、コード側で、 handle_exception を定義してexceptionをすべてcatchして処理を続けたいときもある。
たとえば、現在JSON APIをGoogle App Engine上で作っているのだけど、例外が起きた時もJSONでエラーを返すことにしている。
こういう場合には、その中で JSONを返す処理を書きつつ、logging.exception(exception) と書いておくと、admin画面上では普通にTraceback付きでエラーが見える。
その他
13. 自分のアプリを複数バージョン同時に持てる。ただし datastore は共通。
最初知ったときは、datastore共通で何に使うんだよ、と思ったんだけど色々便利なこともあった。
- 新バージョンのテスト。わりとメインの使い方か。でも、datastoreは一個なのであまり思い切ったことはできない。
- バージョン毎にログが別になる。Google App Engineのadmin画面はバージョン毎なので、ログも別々になる。map/reduceとかを同じバージョンでやるとログがそれで溢れてしまってうざいことがあるんだけど、それも別にしておくといいかもしれない。
- propertyの型を変えるとかちょっとしたmigrationをするときに別のバージョンでやるのがいい(下記参照)。
14. 非同期でいい処理にはできる限り Task Queue を使う
リアルタイムでなくていい処理は全部 Task Queue に回してしまってもいいぐらいかも。
主な理由は二つ。
- HTTPアクセスの処理を早く終わらせるため
これはもちろんユーザのために、という意味もあるが、Google App Engineの設計ではとにかく早く処理を返すことがスケール面でも有効らしいので大事。
- リトライしてくれる
Google App Engineはそれなりに不安定なので、Google App Engineの都合でエラーがそれなりの頻度で起こる。そのさい task queue に入れておけばGoogle App Engineが直るまでretryしてくれる。
特に、mail送信などのAPIはしばしばtimeout (例えばこんなエラー: “DeadlineExceededError: The API call mail.Send() took too long to respond and was cancelled”) とかするので、これは必ず task queue に入れておいたほうがよさそう。メールは元々非同期で問題ないので相性もいい。
15. Modelのpropertyの型の変更
datastore自体はどんな型でもレコード毎に柔軟に持てるのだけど、その上で動くModelクラスは型にとても厳しくなっている。floatからintegerへなどの型も自動的には変換してくれない。その上read時にexceptionを出してくれてしまうので、結構厄介。
運用を初めてから型を変更する必要がでてきた場合には、以下の方法がいいみたい。
db.Expando クラスを使って値を更新する。db.Expandoクラスは、Modelのサブクラスなんだけど、設定していないpropertyも読み書きができる。また、システムを止めずに更新したい場合には、アプリの別バージョンを作成し、その上で行うと影響をあまり与えずにできる。
例:
class HogeUser(db.Model):
foo = db.StringProperty()
bar = db.StringProperty()
score = db.FloatProperty()
このscoreをfloatからintegerに変更したくなったとする。
こういう場合には、このクラスを一時的にdb.Expandoから継承するようにして、かつscoreを削除してしまう(データ自体は消えない)。
class HogeUser(db.Expando):
foo = db.StringProperty()
bar = db.StringProperty()
# score = db.FloatProperty()
そして、以下のようなmap/reduceを走らせる。
def alter_score_to_int(hoge_user):
if "score" in hoge_user.dynamic_properties() and type(hoge_user.score) is float:
hoge_user.score = int(hoge_user.score)
hoge_user.put()
終わったら、元に戻して Int に変更する
class HogeUser(db.Model):
foo = db.StringProperty()
bar = db.StringProperty()
score = db.IntegerProperty()
16. エラーをメール通知
http://code.google.com/appengine/articles/python/recording_exceptions_with_ereporter.html
標準で入っているこのライブラリを使うと、発生したエラーをdailyでまとめてメールしてくれる。それほど高機能ではないが、同じようなエラーはまとめるという最低限なことはしてくれる。自分でメールの内容をカスタマイズもできるみたい。
こんな感じで最初の方に書いている。ローカルでのSDK環境の場合、エラーになってしまうのでスキップしている。
in_local_sdk = (os.environ.get("SERVER_SOFTWARE") == "Development/1.0") or os.environ.get("TERM")
if not in_local_sdk:
from google.appengine.ext import ereporter
ereporter.register_logger()
その他の設定は、上のリンク先のまま。
17. 死活監視
Google App Engine上のアプリの死活監視をする場合の注意点として、Google App Engineが不安定になっている場合、readはokで、writeだけがエラーになるケースがあるようなので、writeが発生するような処理で監視をしたほうがよさそう。
参考にしたところ
http://www.mail-archive.com/google-appengine@googlegroups.com/msg20008.html
http://stackoverflow.com/questions/4742875/change-integerproperty-to-floatproperty-of-existing-appengine-datastore
http://devblog.miumeet.com/2011/02/schedule-mapreduce-daily-on-appengine.html
http://googleappengine.blogspot.com/2009/06/10-things-you-probably-didnt-know-about.html
他多数、だが忘れた。
やめちゃった。
8年半前に創業直後だったこの会社に入って、ほぼ0から色々なものを作り、動かし、それなりに会社は大きくなり、立場的にもそれなりに偉くなり、給料もそれなりに貰って、居心地は悪くなくて、そうなんだけど、その一方で、自分にとって刺激は少なくなり、なんとなく楽しさが減っていた、という感じ。自分自身なんとなく安定してしまっている、という状況自体がなんとなく怖かったりもした。
それでも、暇なら色々と遊べるのだけど、日々やらないといけないことは全然減らなくて、忙しいんだけど何か刺激がない、みたいな悪い状態に陥っていた。
会社のことを考えてみても、多少老害になりそうな自分が辞めることで、新陳代謝したほうがいいのかなー、というのもある。よく知っているだけに思い切ったことができなくなっていることも多いし、なんとなく偉そうにしている自分も嫌だったりした。
前にも少し書いた ように基本的にいい会社になっているとは思うんだけど、悪い文化もできてしまっている。その悪い文化を作ることになってしまった一端は、長いこといる自分にもあるわけで、その自分が存在しつつ、その悪い文化を変えていくのはなかなか難しい、と感じているのもある。
まあ、上記すべて本音のつもりだけど微妙に嘘っぽく、後付けの理由っぽくもある。単に、色々なことに飽きてしまったのかも!
家族もいるし、常に崖っぷちを楽しむわけにもいかないけど、それなりの逃げ道を残しつつも新しいことドキドキしながらしたいとか思ってしまう。こんな時代だし、飽きちゃったとか、新しいことしたい、とかいう理由でやめちゃうのは甘っちょろいな、とも思うんだけど、まあやめちゃいました。
なんだかんだで8年半在籍したわけだけど、8年半ってのは結構長いよなあ。自分の中でも、人生で所属した組織で一番長いものになってしまった。2位が小学校で、3位が最初に入った会社かな。そういう組織を離れるのはなかなか寂しいものはあるのだけど、別れはいつか来るということで。
それにしても、この会社では本当に、大きなことから小さなことまで色々な新しい経験をさせてもらったなー。外国文化に触れることもできたし、とても人生にとってプラスになるものだったと思う。感謝でいっぱい。
で、何するの? ってことだけど、まだ内緒! ということで。もう少しロンドンにいる予定です。