はじめに
O`REILLY発行の マイクロサービスアーキテクチャ を読みました。
非常に興味深く、これまで主に企業向けエンタープライズ開発に関わってきた自分にとっては、今後のソフトウェアエンジニアとしてのキャリアを作っていくには、考え方の転換が必要だと気付かされる内容でした。
本書の内容は広範囲に渡りますが、特に重要と感じた要点を整理(かなり長いですが)しておきたいと思います。(後編 との2部構成です)
要点
はじめに
- マイクロサービスを取り巻く技術の進歩は早く、マイクロサービスを実現する特定の技術を習得することよりも、その本質的な考え方を理解することが重要である。
1章.マイクロサービス
マイクロサービスとは
-
マイクロサービスはSOA(サービス指向アーキテクチャ)のひとつの実現形態であり、DDD(ドメイン駆動設計)、CI・CD(継続的インテーグレーション・デリバリ)、インフラ仮想化、自動化、アジャイル開発プロセス、といったさまざまな分野の技術や方法論を組み合わせることで成り立っており、これらが総合的に実践できなければその価値を享受できない。
-
DDDの**「境界付けられたコンテキスト」が、ひとつのマイクロサービスの単位**になる。
-
マイクロサービスは、外部から利用されるためのAPIを公開する。その公開APIの呼び出し元(=コンシューマ)に影響を与えることなく単独で変更・本番リリース可能である。
-
あるマイクロサービスが停止しても、**他のマイクロサービスは停止せずにサービスを提供し続け、**システム全体としては、一部機能を切り離して稼働し続けることができる。
-
マイクロサービスは、必要なサービスだけを、必要なときにスケールさせることができる。
-
マイクロサービスは、**自律的なチームによって開発され、所有・維持される。**採用する技術や方法論は所有チームが自由に決定する。
-
マイクロサービスは、簡単に捨てたり、作り直したりできる。(2週間で作り直せる程度の大きさ)
2章.進化的アーキテクト
マイクロサービスのアーキテクト
-
アーキテクトは、個々のマイクロサービスの内部詳細より、各マイクロサービス同士がどう連携するのか、それらのマイクロサービス全体の状態をどう監視するのか、と言ったシステム全体に注力すべきである。(内部詳細は、所有チームに任せれば良い)
-
マイクロサービスアーキテクチャでは、予測が困難な状況の下で多くのトレードオフな判断が必要になる。目標と、それを実現するための原則とプラクティスを定めることで対処できる。
- 戦略的目標・・・会社や部門レベルで定める、儲けるためには何がどうあるべきか、と言う目標。
- 原則・・・戦略的目標を達成するために、関係者が守るべき規則。
- プラクティス・・・原則を守るための具体的な手段、技術的な標準ルールなど。
-
各マイクロサービスで共通の標準として、最低限以下のようなことを定めておく。
- 各マイクロサービスと、システム全体の状態を監視するための仕組みやルール。
- マイクロサービスがAPIを公開するための標準技術やルール。
- あるマイクロサービスの障害がシステム全体に波及しないようにするための仕組みやルール。
-
マイクロサービスを開発したチームにサービスを所有させ、ビジネス要求から本番運用までの一連のタスクを持たせることで、チームに自律性が生まれる。(タスクでチームを分けるべきではない)
3章.サービスのモデル化方法
-
良いマイクロサービスを設計するには、他のサービスに影響することなく独立してデプロイできる疎結合性と、ドメインの境界づけられたコンテキストでまとめられた高凝集性がポイントになる。
-
経験のない新しいドメインを扱う場合、最初からコンテキスト境界を見極めてサービスの単位を決めるのは難しいので、まずはモノリシックに作って、ドメインの理解が進むにつれて徐々にマイクロサービスに分離していくほうが安全。
-
コンテキスト境界を分離するときは、データの観点ではなく、そのコンテキストが外部に公開する振る舞いに注目したほうが良い。
4章.統合
-
マイクロサービスにおいては、各サービスをどのように結合するか?が最も重要な技術要素となる。
-
可能な限り、サービスの呼び出し元に影響せずに変更・デプロイできるような結合方式を模索する。
-
サービスが外部に公開するAPIは、特定の技術に依存しないようにすることで、呼び出し元の技術選択の自由度を確保するとともに、進歩する技術を随時導入できるようにする。
-
データベース(および、その他のデータストア)の共有によるサービスの連携は、簡単ではあるが疎結合性と高凝集性の両方を失うことになり、マイクロサービスを採用する意味がなくなるので避けるべき。
サービス間の連携方法
- サービス間の通信方法として、同期か非同期がある。
連携方法 | 同期 | 非同期 |
---|---|---|
リクエスト/レスポンス | サービスを呼び出してその応答が返ってくるのを待つ。 | サービスを呼び出す際にコールバックを渡して即時にリターンし、サービスの処理が完了したらコールバックを呼び出してもらう。 |
イベントベース | なし | サービスを呼び出すのではなく、何らかの事象(イベント)が起こったことを通知する。そのイベントに関心のあるサービスは能動的にそれを受信して処理を行う。(イベントを発生させたサービスは、誰がそのイベントを受信するかは感知しない) |
- あるサービスとその下流となる複数のサービスによってひとつの処理が構成される場合の制御方法として、オーケストレーションかコレオグラフィがある。
方式 | 説明 |
---|---|
オーケストレーション | 基点となるサービスが他のサービスをリクエスト/レスポンスで呼び出し、処理全体のフローを制御する。基点となるサービスにロジックが集中してしまい、サービスの独立性が失われがちになる。 |
コレオグラフィ | 基点となるサービスがイベントベースでイベントを発行し、他のサービスがそれを受信して個々に処理を行う。 マイクロサービスの原則を満たすには、コレオグラフィによる連携を模索すべき。 |
- コレオグラフィによる連携では、全てのサービスが処理を完了したのか、どこまで処理が進んでいるのか、と言ったイベントの処理状況を監視・追跡するための仕組みづくりが必要になる。
リクエスト/レスポンス
-
サービス間の通信プロトコルとしては、HTTP(S)+RESTがデフォルトの選択肢となる。
- さらにHATEOASを採用することで、サービスの利用者は個々のAPIエンドポイントを把握する必要がなくなる。このことで、サービスが公開するAPIエンドポイントを柔軟に変更できるメリットがある。(が、まだ積極的な採用事例は少ない)
-
データフォーマットとしては、JSONが多数派。
イベントベース
-
イベントベースのサービス間通信を実現するために、あるサービスがイベントを発行(パブリッシュ)し、他のサービスがそのイベントを受信(サブスクライブ)するための基盤の構築が必要になる。
-
受信したイベントの処理に失敗した場合を考慮し、その仕組みを構築する必要がある。
- 失敗したイベントの処理のリトライ。何回リトライするか、どれくらいのインターバルを置くか。
- リトライしても最後まで成功しなかったイベントの扱い。エラーキューに移動させて、後で別途リカバリできるようにするなど。
- エラーキュー内のイベントの監視/参照とリカバリ。個々のマイクロサービス独自ではなく、標準的な仕組みがほしい。
-
複数のサービス、イベントキューにまたがって処理が連動していくので、それらを紐付けて追跡できる仕組みがないと運用できない。一連のイベントの最初にユニークな相関IDを発番し、それをイベントに乗せて伝播させるようにする。
コードの統合
-
ロギングなどの汎用的なものを除き、マイクロサービス(コンテキスト境界)をまたいでコードを共有しないほうが、個々のサービスの独立性を維持できる。
-
サービスが公開するAPIを利用するための*クライアントライブラリ(SDK)*を提供する場合は、コンテキストのロジックがクライアントライブラリに漏れないように注意する。また、クライアントライブラリのバージョンアップは利用者が任意のタイミングで行えるようにする。
-
公開したサービスの仕様を変更する場合は、可能な限り下位互換性を保ち、呼び出し元への破壊的変更を回避する。
- 破壊的変更を伴う場合は、一時的に新旧バージョンを並行運用して呼び出し元が新バージョンに移行する期間を持たせる。
ユーザーインターフェースとの統合
複数のマイクロサービスの結果を1つの画面に表示させる場合、いくつかの方法がある。
方法 | 概要 |
---|---|
API合成 | XMLやJSONデータを返す複数のサービスを画面から呼び出し、フロントエンドで画面を生成する。 |
UI部品合成 | サービスがUI部品を生成して返し、フロントエンドでそれらを組み合わせて画面を構成する。 |
BFF(Backend For Frontend) | サーバー側に、複数のサービス呼び出しを画面向けにまとめるゲートウェイ層を設ける。 |
レガシーシステムとの統合
- 既存システムやプロダクトと統合する場合は、その既存システムへの呼び出しを**マイクロサービスでラップし、徐々に既存システムを新しいサービスに置き換えていく(ストラングラーパターン)**ことでビッグバンな変更を避けてマイクロサービスを導入できる。
5章.モノリスの分割
-
モノリシックなシステムをマイクロサービスに分割する際は、境界づけられたコンテキスト で分割する。
-
一気にマイクロサービス化するのではなく、**影響の小さい機能から徐々にマイクロサービスに分割していく。**こうすることでチームがマイクロサービスを学習する機会を設け、導入のリスクを減らす。
データベースの分割
モノリシックなシステムを分割するには、コードの分割だけではなく、データベース(RDB)も分割する必要がある。
-
境界づけられたコンテキストをまたぐテーブル共有はせず、サービスを介してデータをやり取りする。ただし、これによって外部キーによるデータの関連やトランザクション整合性を保証できなくなる(=結果整合性になる)。
-
**データ(メタ的なデータなど)もコンテキストをまたいで共有することはできない。**各サービスで同じデータを重複して持つ、列挙型などのコード化して各サービスに取り込む、共有データを扱う独立したサービスにするなど。
-
**データベースの分割は段階的に。**まずサービスはモノリシックのままでコンテキストごとにデータベースを分割し、次にサービスを分割する。
-
データベースを分割することでデータが結果整合性となる場合は、その整合性が取れていない状態を表現するドメインの概念(例 処理中の注文など)を導入する。
帳票の分割
一般的に帳票には複数のコンテキストの情報が集約される。サービスを分割することで帳票のために情報を集約する方法を検討する必要がある。
-
各サービスを呼び出して帳票で必要な情報を収集する場合、必要に応じてサービスに情報をバッチ的にまとめて取得するAPIを設ける。
-
帳票用のデータベースを設けてサービスのデータを連携する(データポンプ)場合、イベントベースでサービスからの変更イベントをサブスクライブして帳票用データベースにデータ投入することを検討する。
6章.デプロイ
モジュール
-
マイクロサービスのCIでは、個々のサービスを独立してビルド・デプロイできるよう、サービスごとにコードリポジトリやビルドジョブを構成するのが良い。
-
ビルドのサイクルを最適化するために、ビルドにステージを設けてビルドパイプラインを構成する。ステージの最初のほうでは高速に実行できるテストを実施してフィードバックループが早く細かく回るようにし、ステージが進むにつれて低速で総合的なテストを実施するようにする。
-
サービスごとに独立したビルドパイプラインを構成し、ビルドステージの進行を管理できるCI・CDツールの選定が望ましい。
手動で実施する受入れテストもステージのひとつとして管理するべき。
インフラ
-
モジュールが稼働するために必要なインフラやミドルウェアも自動的に構成できるようにする必要がある。
- モジュールを実行環境のプラットフォーム(OS)ネイティブのパッケージ管理ツールにあわせる。
- モジュールとインフラ、ミドルウェアをひとまとめにしたVMイメージを作る。VMイメージの作成は Packer でコード化すると便利。
-
CI・CDによって構成され稼働している**サーバは、原則として手作業で変更(SSH接続したり、管理コンソールを使ったり)してはいけない。**サーバの構成を変更するには、ビルドパイプラインを使ってビルド・デプロイを行う。
設定項目
-
サービスが必要とする設定項目は、DB接続情報のような環境に依存する情報だけとし、必要最小限の項目に留めるようにする。
-
環境依存の情報はモジュールには含めずに設定ファイルなどで別管理できるようにする。
サービスとサーバのマッピング
-
マイクロサービスでは、1台のサーバ(およびアプリケーションサーバのようなコンテナ)に複数のサービスを同居させるべきではない。
- 各サービスを独立してデプロイし、運用状況を監視することが困難。
- 共有されるサーバがボトルネックとなって、サービスを開発するチームの自律性を妨げる。
- サーバのスケーリングが困難。
- サーバを共有するのは、サーバリソースの最適化(節約)が目的であり、仮想化技術などを使えばその必要はなくなる。
-
1台のサーバに1つのマイクロサービスをデプロイすることで、上記の問題はすべて解決できる。
デプロイツール
- モジュールのデプロイは、以下の情報をパラメタとして受け取るコマンドラインツールで実行できるが望ましい。これを手元のターミナルやCI・CDツールから実行する。
- Fabric を使ったPythonスクリプトを作成すると良い。
- モジュールバージョンは、システム全体に対してではなく、個々のサービスごとに発番する。(項7.6.4より)
情報 | 説明 |
---|---|
モジュール | デプロイするモジュールの名前や場所。 |
バージョン | デプロイするモジュールのバージョン。 |
環境 | デプロイ先の環境。 |
- 環境のデプロイ(作成)も、コード化して自動化する。
- Terraform や AWS CloudFormation などを使う。
7章.テスト
テスト
-
マイクロサービスの利点を得るには、テストを自動化してサービスを迅速かつ効率的に検証できるようにすることが不可欠。
-
テストをステージに分け、適切かつ高速にフィードバックループが回るようにする。
- スコープの大きなテストで不具合が見つかったら、より小さいスコープに確認するテストを追加してテストのフィードバックループを継続的に改善していく。
- 必要のなくなったテストは削除して実行時間を短く保つ。
テスト | テスト対象 | テスト作成者 | スコープ | 実行時間 | 備考 |
---|---|---|---|---|---|
単体テスト | サービスを構成する各タイプやメソッド。 | 各サービスの開発チーム | 小 | 小 | サービスは起動せず、ネットワークや他サービスの呼び出しはスタブ/モック化することで、どこでも高速に実行できるようにする。 |
サービステスト | 個々のサービス。 | 各サービスの開発チーム | 中 | 中 | サービスは起動するが、そのサービスから連携される下流サービスはスタブ/モック化する。 |
エンドツーエンドテスト | 各サービスを組み合わせたシステム全体 | サービスの開発チームで共有 | 大 | 大 | 画面などのユーザーインターフェースを介したテスト。 |
コンシューマ駆動契約(CDC)テスト | 個々のサービス | サービスの開発チームと、利用者で共有 | 中 | 中 | 利用者からのサービス呼び出しに破壊的影響がないか?の観点のテスト。 |
サービステスト
- 下流サービスのスタブ/モック化には、スタブサーバを立てるツール mountebank が使える。
エンドツーエンドテスト
-
各サービスのビルドパイプラインをまとめるビルドステージとすることで、サービステストを通過した各サービスを組み合わせて実施できる。
-
テストはサービスの開発チームの共同所有とする。専属のテストチームなどを置いて、開発チームの手からテストが離れるとテストの質が悪化する。
-
全てのユーザーストーリーをエンドツーエンドテストしていてはテストに時間がかかりすぎて素早いリリースが困難になるので、システムの中核だけに絞ってテストを行うジャーニーテストを検討する。
コンシューマ駆動契約テスト
-
利用者からの見た挙動を保証することに焦点を絞ってテストすることで、比較的速いサイクルで利用者に影響する破壊的変更を検知することができる。
-
Pact のようなCDCテストを支援して自動化するツールもある。
-
どれだけテストを厚くしても不具合の混入を100%防止することはできないと認識して、いち早い本番環境で稼働させることによるフィードバックを優先するなら、エンドツーエンドテストよりもCDCテストに重点を置く。
本番環境でのテスト
- リリース前に手厚くテストすることにリソースを使うよりも、本番環境での稼働監視や素早い障害復旧のための仕組み作りにリソースを使い、早く本番環境で稼働させたほうが効率的。
説明 | |
---|---|
スモークテスト | 本番環境にデプロイされたシステムが正しく稼働しているかを確認するテスト。 |
ブルーグリーンデプロイメント | 本番環境上に現行バージョンと並行して新バージョンをデプロイ(トラフィックは現行バージョンで処理)してテストし、問題がなければトラフィックを次バージョンに向ける方法。 |
カナリアリリース | 本番環境上に現行バージョンと並行して新バージョンをデプロイし、トラフィックの一部または全部(のコピー)を新バージョンに向けてテストする方法。 |
非機能テスト
-
非機能テストは厳密には本番環境でなければ検証できないものが多いが、サービス開発のできるだけ初期段階から非機能テストを設計して継続的に実施できる仕組みを構築し、徐々に育てていくほうが良い。
-
**性能テストは大量のデータや負荷状態を作り出すことが必要なため頻繁には実施できない。**日々のサイクルではその一部のケースを実施し、全ケースの実施は週次などで行う。
...8章~12章は 後編 に続きます