レバレッジメモ:継続的デリバリー12章

社内読書会のために継続的デリバリー 信頼できるソフトウェアリリースのためのビルド・テスト・デプロイメントの自動化の12章を予習したのでレバレッジメモ

12章:データを管理する

データのライフサイクルはアプリよりも長い。規模も大きい。

データの構造を変更する必要性は必ず出てくる。混乱を最小限に収めるには?

12.2 データベースのスクリプト処理

継続的デリバリーの最も重要な点は「環境を再現し、その上でアプリを走らせる作業」を自動化するところだ。
DBの初期化やマイグレーションは全てスクリプトにしてバージョン管理する必要がある。

DB更新の最もシンプルな方法は「消して作りなおす」だ。それができるならさっさとそれでやればいい。しかし多くの場合は消してはいけないデータがたくさんある。どうするか?

12.3 インクリメンタルな変更

インクリメンタルなDBの変更のためには、マイグレーションスクリプトにテーブルのバージョンを入れる。
失敗したら戻せるように、ロールフォワードとロールバックの両方のスクリプトを作る。
こうすればあるバージョンのテーブルから別のバージョンのテーブルへどうすれば変換できるかが機械的に判断できる。

データを捨てるような変更をする際は、ロールバックで元に戻せるようにデータをテンポラリなテーブルにコピーしておく。
しばらくして問題無いと判断できてからテンポラリテーブルを消せば良い。

12.3.2 オーケストレイトされた変更を管理する

複数のアプリがDBを共有している場合は、なるべく実運用環境に近い環境でテストするべき。DBに加えた変更が予期しない他のアプリに影響を及ぼす可能性があるから。

コラム 技術的負債

技術的負債は利子を産む。負債を溜めすぎると利子の返済(動かし続けるための保守)しかできない状態になり、新しい機能の追加ができなくなる。

大半のプロジェクトでは負債まみれになる。毎回変更後にリファクタするぐらいの方がよい。未来から借り入れるなら返済計画を立てること。

12.4 DBのロールバックとゼロダウンタイムリリース

本番環境には二つの制約がある。

  • アップグレードを取り消した際に、アップグレード後のトランザクションで追加されたデータを失ってはいけない
  • SLAを守るために、ダウンタイムを最小限にしなければならない

解決策は3つある

1つ目は、トランザクションをどこかのレイヤーで記録しておくことで、後で復元できるようにする方法。DBのテーブルのレイヤーでコピーするのに限らない、操作ログやイベントログやトランザクションログから復元できるならそれでOK。

2つ目は、旧システムと新システムを並行して走らせる方法。新システムが失敗で旧システムに戻したとしても、新システムの中のデータは失われない。

3つ目は、DBスキーマ変更時にはアプリを新バージョンのDBスキーマと旧バージョンのDBスキーマのどちらでも動くように設計する方法。これによってアプリの更新のタイミングとDBスキーマの更新のタイミングを切り離すことができる。アプリを旧バージョンに戻したとしてもDBは新しいままなのでデータが失われない。

P.414で紹介されているDBアクセスに抽象化レイヤーを挟む方法もアプリをDBスキーマの変更から守るために使える方法だ。と著者は言ってるけどDBスキーマ変えるのはだいたいアプリが何かその値を読み書きしたいからだよね。DBスキーマ変更のたびに抽象化レイヤーのメンテコストが掛かるじゃん。

12.5 テストデータを管理する

テストデータの管理には2つの問題がある。

  • テストのパフォーマンス
  • テストの分離

パフォーマンスのためには実データを使うな。実データはテストの目的に比べて大きすぎる。
DBアクセスを抽象化レイヤーで切り離し、テストダブルで置き換えられるようにする。もしくはSQLiteなどのインメモリDBに差し替えられるように設計する。

テストの分離のためには、テスト用のデータがそのテストからしか見えないことが重要。
それを保証するために筆者は「トランザクションを開始してから操作を行い、テスト終了後にロールバック」という方法を使っている。

筆者はテストがお互いに影響し合わないことを「テストがアトミックである」と表現しているけども、データベースやトランザクションの話をしている文脈でACIDのAでないものをアトミックとか呼ぶのは誤解を招くので良くないと思う。筆者が気に入っているテクニック「トランザクションの中での操作は他から見えない」はアトミシティじゃなくてアイソレーションじゃん。

「Xをして、それからYをして…」というストーリーを作って、それにそったテストをしたくなる。個々のテストごとの初期化コストなどが必要なくなるし魅力的に見えるが、テストが密結合になるので将来的にメンテコストが跳ね上がる。将来的に何かが変更されてストーリーテストがコケた時に、どこがどういう挙動をしていてどこを直すべきなのか調査が必要になる。結局各ステップごとのテストをやるのと変わらない。

12.6.1 コミットステージでのテストにおけるデータ

テストと実装が密結合過ぎると、テストが実装の変更の妨げになる。本当はリファクタ時のミスからの防波堤でなければならない。実装の些細な変更でテストを大幅に変更する必要が出るなら、そのテストは「ふるまいの仕様」としての役割を果たせていない。

テストと実装の密結合は、データに凝りすぎた結果であることが多い。最小限のデータを使ってふるまいを確かめる。データの作り方に注意し、可能な限りテストヘルパーやフィクスチャを使う。こうすればデータ構造が変わってもフィクスチャの修正だけで済む。テストを全部直すはめになったりしない。

「各テストに固有のデータ」を最小化することを目標にする。

12.6.2 受け入れテストにおけるデータ

テストに固有でないデータはダンプで保存してバージョン管理する。マイグレーションのテストにも使える。

12.6.3 キャパシティテストにおけるデータ

P.299 インタラクションテンプレートを使って記録したものを再利用

12.6.4 その他のステージにおけるデータ

重要なコンセプトは「ふるまいの指定」を再利用すること。

本番データのダンプは大きすぎる。目的に合わせて適切なサイズのいろんなダンプを用意しておく。

12.7 まとめ

  • 重要なのはDB作成やマイグレーションを完全に自動化すること。
  • 本番データのダンプは大きすぎる。
  • DBをバージョン管理する。マイグレーションを自動化する。
  • スキーマの前方・後方互換性に気をつけて、DBのデプロイに関する問題をアプリのデプロイに関する問題と切り離す。
  • テストが共有するデータは最小限にする。

もちろん個別の事情に合わせて変化させる必要があるが、まずはこれが大原則である。