2012-07-29

Jenkins ユーザ・カンファレンス 2012 東京で話を聞いてきました


John Smart「Jenkins によるよる自動受け入れテストから継続的デリバリーまで」


ビジネスゴールに貢献する価値に集中する。ビジネスに貢献しないことは、すべて無駄だ。
Business Goal -> Features -> Stories -> examples へブレイクダウンしていく。

これはリーンスタートアップや、アジャイル開発なんかの考え方に近いと思います。

受け入れテストの自動化が、リリースサイクルを速くする。
メトリクスがしきい値を下回ったら、ビルド失敗にする。たとえば、コードカバレッジなど。

テストを自動化する時間 < 手動でテストをする時間、が成立するというのが条件だと思います。でないと、抵抗にあうんじゃないでしょうか。その見極めが重要だと考えています。

もうひとつ気にしているのは、そういう受け入れテストをしやすい設計にする、という仮説です。作りやすさではなくて、テストのしやすさ、すなわち、価値の測りやすさを、設計の方針に組み込むのがいい、という仮説を持っています。

出荷可能であるために、品質が高いこと、そして、品質が高いことを確信しているとことが大事。
どのくらいの品質なのかを知っている、という状態にしたいのです。バグがあるのが分かっていて、それがどの程度プロダクトに影響があるのか、そして、ビジネス価値に、どの程度影響するのか、が前もって分かっていて、受け入れ可能であれば、リリースできると思っています。ので、こういう考え方には、勇気づけられました。

R. Tyler Croy 「飛行機を飛ばしながら直す方法教えます:JenkinsとGerritによる継続的デプロイメントの実践」


反復的に構築していくプロセスなのである。

手動でデプロイしている会社に入って、開発を続けながら、継続的デプロイメントを浸透させていった過程の話でした。これを一番聞きたいのです。

リリースの何%が失敗して、リリースあたりどのくらいコミットがあって、とかを最初に測定しているのがすごいですね。でないと、効果が分からりませんからね。

7台並列でやって、テストが終わるまで15分かかる。これは、本番サーバに問題があってたときに、修正するまで最大で15分かかるってことだ。

なるほどなぁ。テストに時間がかかるということは、ビジネス価値の提供が遅れるということでしょう。ビジネス価値につなげる考え方を意識せねばなぁと思います。

データマイグレーションが難しかった。データマイグレーションが許される期間があって、その期間以外はマイグレーションがあったら fail させる。マイグレーションが発生したときには、人間がレビューするようにしている。100% 自動化するのは、テストやステージング環境のデータは、本番と違うので、無理だろう。なので人間の目が必要。

ここはやっぱり難しいですよね。KVS の場合はスキーマによる制約がないので、以前書いたエントリのような、アプリケーションでのアプローチが使えますが、RDB だと難しいですね。

まとめ

絶対完璧なプロセスをいきなり導入しようとするのではなく、反復的にだんだん作っていくこと、ビジネス価値に集中すること、がふたりの共通点だったと(勝手に)思いました。というわけで、私も徐々にやっていきます。まとまったら、どこかで発表したい。

(追記)
そうそう。むやみにプラグインを使うのではなく、Makefile で動くようにしておいて、Jenkins はトリがで make して通知するようにしておくといい、というアドバイスというか実践をしている、という話を聞きました。Jenkins の設定がいっぱいあると大変そうなので、Makefile で解決するのはいいなぁと思いました。通知に何を使うかを聞いたら、XMPP だそうで。にゃるほど。

Python Developers Festa 2012.07

Python Developers Festa (pyfes) 2012.07 に行きました。完全に個人的なメモです。

今回は発表することもなかったので、知り合いとダベることと、気になっていることをちょろっと試すことにしました。

普段、仕事をするときには、目の前の問題を、できるだけ確度の高い予想期間で片付けたいので、手持ちのカードだけで解決しようとしてしまいます。たかだか数時間ですが、ちょろっと試す機会に使おうと思ったわけです。自宅だと、ついエロ動画サイトとか徘徊しちゃうんでね。

Git

すみません、今更です。とりあえず、空のプロジェクトを作って、いじいじしました。Git と、いつも使っている Mercurial は、ブランチの考え方が違うので 1 on 1 でコマンドが対応しません。(1) add の意味の違いと、(2) push でどのリポジトリに、どのブランチを、っていうのを指定するのが、いちばん私が気をつけねばと感じたことです。

PyCharm

いまは Komodo Edit (Komodo IDE の機能限定無料版)を使っています。以前 @ikasamt に Pycharmいっすよと言われていました。試用版をインストール。

Backspace にCTRL-H 、Enter に CTRL-M を割り当てる。

App Engine のプロジェクトを作ったのだけれど、最近は buildout で環境を使っているので、Komodo も Pycharm の App Engine インテグレーションはじつは不要だったりする。単体テストやデバッガを使うときに効いてくるのかも知れない。

watchdog モジュール

ドキュメントもコードも、ファイルを変更したらビルド/テストするようにしています。が、これが美しくないやりかたをしています。OS X 上でのEmacs や Komodo でのファイル変更は、OMake で検知できません。

watchdog インストールして、さっそくやろうとしたら…

$ bin/watchmedo shell-command --patterns="*.c;" --recursive --command='make all'
Traceback (most recent call last):
…
    from argh import arg, alias, ArghParser
  File "/Users/torufurukawa/works/pyfes201207/lib/python2.5/site-packages/argh/__init__.py", line 25
    from .exceptions import *
SyntaxError: 'import *' not allowed with 'from .'

面倒くさいなぁ。もういいや。

ndb

GvR が作った、Google App Engine のデータストアライブラリ(あるいは、BigTable のラッパ)。この文書をだらだら眺めていました。Pythonic で使いやすいし、隠蔽のバランスが直感的です。

ところで未だに、キーをヒエラルキーにすることが活かされる状況って、どういうときだよ、と思っていました。トランザクションで要るとは思うんだけどなぁ、などと、昼ごろにうとうとしながら考えていました。

あー、エンティティではなくて、キーから親子関係を引けるのがいいのか、と。
キーに親子関係がないときは…

from google.appengine.ext import ndb
class Foo(ndb.Model):
    pass

class Bar(ndb.Model):
    foo = ndb.KeyProperty(kind=Foo)

f = Foo()
f.put()
b = Bar(foo=f.key)
b.put()
k = b.key

# k から f を取得する。
b = Bar.get_by_id(k.id())  # ここでフェッチ
f = b.foo.get()  # ここでもフェッチ

キーに親子関係があると

from google.appengine.ext import ndb

class Foo(ndb.Model):
    pass

class Bar(ndb.Model):
    pass

f = Foo()
f.put()
b = Bar(parent=f.key)
b.put()
k = b.key

# k から f を取得する
f = b.key.parent().get()  # フェッチはここだけ

だべり

  • とおるメモを、電子書籍にして売れば? → いいなぁ。このブログのアフィリエイトよりも、遥かに効率がよさそうだ。
  • とおるメモアプリは、電話機能にアクセス権限とか要るんでしょ? → w
  • アイコンはち◯こだろ → w
  • うちの製品のマーケティングって、次は何したらいいの? → (・∀・)ニヤニヤ

2012-07-18

データモデルが変わるときのデプロイ

運用中のウェブサービスで、保存してあるデータのスキーマを変えたいときがあります。このとき、プログラムのコードとデータベースの間で互換性が保ちつつ、双方を更新する必要があります。Google App Engine を使ったサービスでのやったことのメモです。



プロパティの増減

最初、こんなデータがあったとします。

from google.appengine.ext import db

class User(db.Model):
    name = db.StringProperty()

これでたくさんのデータが保存されている状態になったとしましょう。ここで、プロパティが増えます。

class User(db.Model):
    name = db.StringProperty()
    birthday = db.DateProperty()

Google App Engine のデータストアには ALTER TABLE みたいなのがありません。が、こういう場合はデータストアに何もしなくていいです。birthday を指定せずに保存されたエンティティは birtyday に None が入っています。

プロパティが減る場合は、単純にモデルの定義からプロパティを消せばいいでしょう。データストアには残っているけれど、あっても害がないから。けど、後々、違う型で復活したりすると、よろしくない気がします。

クエリが増える

プロパティを変更したととき、あたらしいクエリを発行することになった場合には、 (1) インデックスの更新してから、(2) コードをデプロイします。

これまで使っていなかったけど、新たに以下のようなクエリをするようになったとしましょう。

User.all().query(birthday<xxx).query(name<yyy) 

dev_appserver をローカルで動かして、このコードを一度でも実行すると index.yaml が更新されます。以下のコマンドで App Engine のインデックスを更新します。

appcfg.py update_indexes .

ダッシュボードの Datastore Indexing のページを見ると、Building… と書かれている箇所があります。何度かリロードして、Ready になってから、appcfg.py update します。先にインデックスを更新しないと、アプリケーションコードがクエリしようとしたとき、「インデックスができてねーよ」エラーが出ます。


より複雑な変更


ほんとに困るのは、プロパティの型が変わってしまうとか、複数のプロパティでひとつの状態を表したくなるとかいう場合です。


class User(db.Model):
    name = db.StringProperty()
    birthday = db.StringProperty()  # DateProperty から StringProperty へ変更

このコードをデプロイして user.birthday にアクセスしたとき、以前の DateProperty で保存されていると、ここでエラーが出ます。だからといって、先にデータを書き換えてしまうと、現行コードでエラーが出ます。

書籍「継続的デリバリー」は、新旧どちらスキーマでも動くようにアプリケーションを変更したあと、データベースのスキーマを変更する、という方針を提案しています。 Google App Engine のデータストアであれば、新旧どちらのプロパティの型であってもいけるようなモデルに変更する、ということになります。

class User(db.Model):
    name = db.StringProperty()
    date_birthday = db.DateProperty(name='birthday')
    str_birthday = db.StringProperty(name='str_birthday')

    def get_birthday(self):
        if self.date_birthday:
            return self.date_birthday.strftime('%Y-%m-%d')
        else:
            return self.str_birthday

    def set_birthday(self, val):
        if isinstance(val, date):
            self.str_birthday = '%Y-%m-%d' % (val.year, val.month, val.day)
        elif isinstance(val, str):
            self.str_birthday = val
        else:
            raise TypeError
        self.date_birthday = None

    birthday = property(get_birthday, set_birthday)

ハンガリアンっぽくてキモいです。あと文字列に変換するところは、関数に切り出したほうがいいですね。で、これでデプロイします。データの個数が少なければ remote_api で、多ければ mapreduce のライブラリを使って、プロパティの型を変更します。すべてのデータが新しい型に対応したら、不要なコードを取り除いてデプロイします。

class User(db.Model):
    name = db.StringProperty()
    birthday = db.DateProperty(name='str_birthday')

と、偉そうに書いたものの、最後の書き換えをせずに残っている箇所もあります。