サーバーレスアプリケーションについて書かれたものの多くは、詳細に欠けていて、サーバーレスが他のソフトウェア構築手法よりも本当に優れている理由が厳密にはわからない。あるいは、詳細に入り込みすぎて、細かな考察がサーバーレスではない他の実装よりもどう優れているのかわからない。この記事では、サーバーレスが他のアプリケーションアーキテクチャとどう異なるのかを定義して説明する。次に、サーバーレスアプリケーションのアーキテクチャは(適切に用いれば)非サーバーレスアーキテクチャよりも優れていることを示す「証拠」のようなものを見ていく。最後に、アーキテクトや開発者がサーバーレスのメリットを実現するのに役立つ経験則で締めくくる。この記事はQCon NY 2018とServerlessconf SF 2018で著者が紹介した概念と例をふくらませたものだ。
何のために最適化しているのか?
平均的な開発者について考える
もしソフトウェアが世界を飲み込んでいるなら、ソフトウェアを構築・展開する能力が、ますます組織の成功を推進することになるだろう。現代のソフトウェアの大部分は、通常、SaaS(Software-as-a-Service)として提供されるため、ソフトウェア開発は永久に続いていく。組織は時間をかけて、ソフトウェアの開発リソースを増やしていく必要がある(事前の大規模な設備投資とはるかに小さな継続的保守費用を必要とするビル建設とは対照的だ)。そのため、長期にわたり優れたソフトウェア開発ベロシティで動ける組織は、そうでない組織よりも優れているはずだ。
組織の成功のために最適なソフトウェア開発組織を構築したければ、平均的なソフトウェア開発者がソフトウェア開発を維持しやすいような選択をしなくてはならない。組織がリーズナブルな開発ベロシティを維持できない場合、その主たる苦痛の種は、通常「技術的負債」と呼ばれている。その原因は、短期的な思考(保守性を考慮せずにコードを書く)や将来の開発者の能力に対する仮定(コードを書いたチームが永久にその開発を続けるという仮定)に関係している。
より少ないコード、より少ない複雑さ
ソフトウェアを長期的に開発し続けられるように最適化したければ、2つの基本戦略に従うべきだ。カスタムコードを少なくすること、そして複雑さを減らすことだ。通常、保守を必要とするコードが少ないほど、保守しやすくなる。この戦略はカスタムコードについて言っていることに注意しよう。よく文書化され、よく保守されているライブラリやサービスを使う場合、自分でコードを書いて保守するという苦痛は増えることはない。
ソフトウェアを保守しやすくするもう1つの高度な戦略は、コードベースにある複雑さを減らすことだ。複雑さについて考える一番の方法は、循環的複雑度(cyclomatic complexity)という概念だろう。これはデプロイメント全体に当てはまる(カスタムコードだけではなく)。エンドユーザーのために特定の経路を動かすのに必要になるソフトウェアの各種パーツ、システム管理、データベース管理、ネットワーク管理、IT運用など、これら全てを考慮する必要がある。経路に必要なパーツの数を減らせば、保守性は高まるはずだ。
サーバーレス
現在のグリーンフィールドソフトウェア開発において、少ないコードと少ない複雑さにより、長期のソフトウェア保守性のために最適化する最善の方法は、サーバーレスソフトウェアアーキテクチャを使うことだ。
定義
サーバーレスの定義には様々なノイズがある(「でも、まだサーバーがあるじゃないか!」「実にひどい名前だ」)。しかし、次のことを意味しているとわかれば、その名前は適切で意味があると思うだろう。
- サーバー運用は不要である
- 稼働時間は自分のものではない
サーバーレスは、SaaSにおいて過去25年たどってきた道の最後のステップだ。私たちはゆっくりと、責任をサービスプロバイダーに引き継いできた。自社のデータセンターからコロケーション施設、専用のホスティング、IaaS(Infrastructure-as-a-Service)の仮想マシン、コンテナへと移行してきたが、いずれもある程度のシステム作業を必要とした。当初は、キャパシティ管理(および設備投資)をはじめ、多くの問題を抱えていた。しかし、たとえコンテナを使っても、パッチ適用やサードパーティ製アプリケーション(Webサーバーなど)の障害処理など、依然として問題は残ったままだ。
サーバーレスは、個別のサーバー運用数をゼロにする。全てがマルチテナントであり、サービスプロバイダーが動かしているためだ(注意: 私の定義によると、独自のKubernetesクラスターで関数を実行するのは「サーバーレス」ではない)。サーバーレスでは、ハードウェアのセットアップ、設備投資、OSのインストール、パッチ適用、アプリケーションのインストール、サードパーティ製アプリケーションの障害といったものを一切扱わない。
「サーバー運用は不要である」ことから、当然のごとく「稼働時間は自分のものではない」。(自分のコードのエラー以上に)動いているかどうかは、サービスプロバイダー次第だ。多くの人にとって、これは恐ろしいことだ(プロバイダーを信じていないためか、自分たちの仕事が不要になることを恐れているためだろう)。しかし、あなたの目標がより少ないコードとより少ない複雑さなら、サーバープロバイダーが信頼できる限り、これはその実現にとって最善の方法だ。
適切なサーバーレスのメリット
全てのサーバーレスアーキテクチャが同じわけではない。典型的な3層モノリシックアプリケーションよりもずっと保守しにくいサーバーレスアプリケーションを構築するのは可能だ。しかし、正しく行えば(これについては後ほど詳しく述べる)、サーバーレスアプリケーションはサーバーレスで実行できない他のアプリケーションアーキテクチャよりも、かなりメリットをもたらしてくれる。少ない相互依存性、少ない負債、定評ある効率の良いマイクロサービスアーキテクチャ、全員のための分離された本番同等の環境などだ。
適切なサーバーレスアーキテクチャを使うと、相互依存性は小さくなる。なぜなら、アプリケーション全体の複雑さが低下するためだ。サーバーレスアプリケーションは、アプリケーションのフロントエンドコード、バックエンド機能、サードパーティ製サービス(例えばTwilio、Algolia)、および、それら機能とサービスの設定から構成される。サーバーレスアプリケーションでは、OS、Webサーバーなどのサードパーティ製サーバーアプリケーション、単一のVMやコンテナに特化したデプロイメントコードといったものを一切扱わない。このため、VMやコンテナベースのアプリケーションと比べて、サーバーレスアプリケーションに取り組んでいる開発者は、別のチームや別のチームメンバーを待ってブロックされることが少なくなる。
ソースコードは負債だ。サーバーレスアプリケーションは、アプリケーションアーキテクチャの中で最もコード量を少なくすることができる。次のセクションでは、最も効果的なサービスを活用することでどれだけコードが減らせるか、例を見ていこう。
Martin Fowler氏は、アプリケーションをマイクロサービスに分解しようとする前に、まずはモノリシックアーキテクチャから始めようという賢明なアドバイスをしているが、自動でスケール可能なバックエンドの機能を使ってシッククライアントへ移行すると、堅牢なマイクロサービスアーキテクチャが無料で手に入る。多数のユーザーへスケールする必要がある場合、アプリケーションをスケールさせる際の苦痛はずっと小さくなる。すでにフロントエンドを機能から分離し、機能を互いに分離しているためだ(この記事のアドバイスに耳を傾けていれば!)。
最後に、サーバーレスアプリケーションはアイドルコストなしで使用ごとに課金されるため、VMやコンテナベースのアーキテクチャで全ての開発者に単一の開発環境を提供するよりも、全ての開発者に別々の本番と同じ環境を提供する方が安価になる。これは開発者のベロシティ(共有の環境を台無しにして他人の邪魔をする開発者はいなくなる)とアプリケーションのデプロイメントコード(全ての開発者にデプロイできるなら自動化!)に大きなメリットをもたらし、環境の違いによるバグを制限する。
証明: 例
サーバーレスで構築できない、また構築すべきでないアプリケーションとして、Hacker Newsで“keepingscore”が提案した具体的なアプリケーション例をみてみよう。
「[この記事の作者は]バックエンドはauthを使った単なる永続レイヤーだと思って運用しています。解決しようとしている全ての問題がその世界観に当てはまるなら、彼に力を与えてくるでしょう。 私は旅行会社のバックエンドのチームリーダーです。フライトとホテルの一覧をAndroidアプリに表示するのは、1回のデータベース検索ではありません。SOAPとRESTを何百回も呼び出し、様々なプラットフォームから航空会社とホテルのデータをつなぎ合わせるのです。これはフロントエンドでやれるものではありません。たとえできたとしても、iOS、Android、Web、社内サポートポータルでこのロジックを複製したくはありません」
アプリケーション
上記の説明を使って、1つのバックエンドとやり取りする、Webサイトと2つのモバイルアプリフロントエンド(AndroidとiOS)からなるアプリケーションを考えてみよう。バックエンドは、ユーザー管理、フライトとホテルの旅程を検索・購入・共有する機能、内部会計に必要なレポートレイヤーを提供する。
具体的には、次のようなハイレベルの機能セットを検討する。
- ユーザー管理
- サインアップ、サインイン、サインアウト、パスワード忘れ
- ユーザープリファレンスの保存
- UI: 検索と購入
- オートフィル基準(例えば、空港コード)
- 検索基準の送信
- 支払い
- UI: 共有と修正
- 表示、他のユーザーとの旅程の共有
- 旅程の変更(キャンセルを含む)
- バックエンド: 検索と購入
- 複数のAPIを並行して叩く
- 応答を結合する、フィルターを適用する
- 旅程を購入する
- バックエンド: レポート
- 購入、旅程に関する分析レポートを実行する
適切なサーバーレスアーキテクチャが、他のアーキテクチャでは提供できない方法で、上記のメリットを提供することを示すため、様々なアーキテクチャについて見ていこう。クライアントについてはここで話す必要がないことに注意しよう。なぜなら、全てのクライアントサイドコードは、異なるアーキテクチャ間で同じだと想定できるためだ。唯一異なるのは、バックエンドの実装方法だ(動作している限り、エンドユーザーはバックエンドの実装方法を気にしないことに注意しよう)。
アーキテクチャ 1: 典型的な3層
典型的なモダンな3層アーキテクチャでは、アプリケーションサーバーを介してキャッシュ層を持つリレーショナルデータベースを使用する。サーバー運用の観点から見ると、Webサーバーおよびデータベースソフトウェアのプロビジョニングとパッチ適用、イメージのブートストラッピング、イメージの焼き込み、コンテナ/VMのフェイルオーバーといったタスクが必要になる(たとえコンテナを使っていても)。VMや自分のマシンを使っている場合、タスクはさらに増える。
加えて、フロントエンドを動かすには通常、カスタムコードが必要になる。私は様々なサンプルアプリケーションを調べて、コード行数の見積もりを考えた。このアーキテクチャの場合、バックエンドを次のように見積もった。
検索API & 結合 |
10,000 |
自分の旅程の表示 & 共有 |
7,500 |
アプリケーションのスキャフォールディング |
5,000 |
ユーザー管理 |
5,000 |
レポーティング |
5,000 |
購入 |
2,500 |
UIの入力補完 |
1,500 |
合計 |
36,500 |
アーキテクチャ 2: FaaS(Functions-as-a-Service)のみ
(アイコンはAWSのものだが、各種IaaSプロバイダーにデプロイ可能)
当初AWS Lambdaが2014年に発表されたとき、サーバーレスアプリケーションのアーキテクチャは上記のようなものだった。事実上、アーキテクチャ1のモノリシックアプリケーションを一連の関数(マイクロサービス)に分解し、クライアントの要求に応じて相互に呼び出して、永続化レイヤーとやり取りするものだ。しかし、これが3層アーキテクチャの大きな改善だとは考えていない。いくらかメリットはあるが、新たな複雑さの地雷原になる可能性もあるためだ。
このアーキテクチャのメリットは、3層アーキテクチャに必要だったサーバー運用をなくせることだ。これは真のサーバーレスアーキテクチャだ。
しかし、これら新しい関数/マイクロサービスを全てデプロイして相互に呼び出すことは、大きな問題になる可能性がある。FaaS(Functions-as-a-Service)はアプリケーション内の関数にそのまま相当するわけではないためだ。その呼び出しにははるかに大きなオーバーヘッドがあり、無限ループに陥ると深刻なコストが発生する(呼び出しに対して課金されるため)。また、FaaSのネストをデバッグするのは、デバッガーでアプリケーションをステップ実行するよりもはるかに難しい。そして、この場合、実際にコード行数が大幅に減るわけではないため、アプリケーションの保守性を向上させるコア要件にはならない。
検索API & 結合 |
10,000 |
自分の旅程の表示 & 共有 |
7,500 |
|
|
ユーザー管理 |
5,000 |
レポーティング |
5,000 |
購入 |
2,500 |
UIの入力補完 |
1,500 |
合計 |
31,500 |
アーキテクチャ3: サービスフル・サーバーレス
(アイコンはAWSのものだが、それ以外の選択肢もある)
これはずっと優れたサーバーレスアーキテクチャだ。私はこれをサービスフル・サーバーレス(Serviceful Serverless)アプリケーションと呼んでいる。このアーキテクチャでは、独特である必要がない、あるいは標準機能と区別する必要のないアプリケーションの全ての部分(例えば、ユーザー管理や認証)を、マネージドサービス(例えばAWS Cognito、Auth0、Google Firebase Auth)で処理する。
検索API & 結合 |
10,000 |
|
|
|
|
|
|
|
|
購入 |
1,000 |
|
|
合計 (当初は36,500) |
11,000 |
ちょっと待って、どうやって実現するの?
上記表に対して、一部の人が即座に抱く反応は、不信と懐疑だ。そんなコード数の削減は可能なのかという不信と、それが可能だとして、新機能の要求がやってきたときに、開発を継続できるほどの柔軟性と拡張性があるのかという懐疑だ。これらのアーキテクチャには拡張性があり、極めてコード行数の少ないアプリケーションをもたらすことを納得させるのに、この記事でできることは限られている。あなたがそれを理解する最善の方法は、自分で試してみることだ。
とは言え、コード行数と拡張性がいかに達成できるか、ハイレベルから理解しやすいように、いくつか挙げておくのは役に立つだろう。まず、サンプルのサービスフル・サーバーレスプロジェクトをみて、どのように動いているか調べるとよいだろう。AWSは(2018年11月時点)、Javascriptで702行のWeb (React) バージョンと、Javaで508行のネイティブAndroidバージョンのグループカレンダーアプリケーションを用意している。すぐに動かしてみたければ、AWS AmplifyやGoogle Firebaseを使うことで、1時間以内に立ち上げて実行できるだろう。
次に、サービスフル・サーバーレスアプリケーションの重要なイノベーションの1つは、以前はコードでやっていたことの多くがサービス設定で(サービスによっては、チューリング不完全な言語で)やれるようになったことだ。チューリング不完全な言語は設定に優れており、設定するサービスに固有であるため、最終的にコード行数は少なくなる。
最後に、ローコードプラットフォームに身を捧げるのとは違って、常に以前と同様にやるという選択肢があり、必要に応じて、既存のサービスを利用する代わりに自分でコードを書くことができる。別の言い方をすると、サービスフル・サーバーレスアーキテクチャの最悪のシナリオは、アプリケーションの一部を以前と同じようにやっていることだ。しかし、それ以外の部分(90%のアプリケーションが必要としている部分)は、サービスに追い出すことにより、大部分を自分で構築・保守せずに済ませることができる。
サービスフル・サーバーレスの詳細
サービスフル・サーバーレスの重要なところは、バックエンド機能に対する軽量/小型の「ゲートウェイ」を持つのではなく、クライアントからのAPI呼び出しを処理して、必要に応じて各種サービスおよび関数にルーティングするマネージドサービスがあることだ。Google FirebaseやHasuraもAPIハブサービスを提供しているが(これらのサービスは現在FaaSサービスとはかなり違ったものになっているので注意しよう。選ぶ前に、よく読んでテストしよう)、現在、この種のサービスで最も良い例は、AWS AppSyncだろう。
これらAPIハブサービスの大きなメリットは、カスタムコードを必要とせずに、ほとんどのリクエストを直接あなたの永続化レイヤーとやり取りできるように、ルーティングできることだ。それは正しい。上記FaaS中心のアーキテクチャ、そして3層アプリケーションでは、データベースに対する読み書きは全て、あなたが書いて保守するアプリケーションコード経由で行う必要がある。上記サーバーレスアーキテクチャでは、APIハブがそうしたリクエストの多くを処理してくれる。これにはきめ細かなアクセス制御も含まれる(例えば、https://docs.aws.amazon.com/appsync/latest/devguide/security.html#amazon-cognito-user-pools-authorizationやhttps://firebase.google.com/docs/database/security/user-securityを参照)。
利用可能なバックエンド機能を全てマネージドサービスに切り替えること、そしてAPIハブを利用してデータストアへのシンプルな読み書きを処理することで、カスタムコードの行数は劇的に減り、他のアーキテクチャではできない方法でアプリケーションの保守性は向上する。
反論
あらゆる代替案がそうであるように、たいていの場合、あるリスクを別のリスクに交換しているだけだ。伝統的な3層アプリケーションアーキテクチャ(あるいは多数の2層マイクロサービス)からサービスフル・サーバーレスアーキテクチャへの移行は、アプリケーションの保守性を向上させるが、いくつか新たなリスクを伴っている。ほとんどの場合、これは申し分のないトレードオフだと思うが、それらを無視するわけにはいかない。
自分のアップタイムではない
私のサーバーレスの定義では、サーバーレスアプリケーション内でコントロールできないものとして、具体的にアップタイムを挙げている。データによると、主要なIaaSプロバイダーは平均的(あるいは平均を上回る)IT運用チームよりもはるかに24時間運用を得意としている。そのため、私のサーバーレスアプリケーションを維持するのに(自動フェイルオーバー処理も含む)、社内運用チームに支払うよりもはるかに低料金で問題なくやってくれる。
とは言え、複数のサービスプロバイダーがほぼ100%の時間、全て稼働していることを当てにしたアプリケーションを作ってしまうと、サービスプロバイダーの数が増えるにつれ、アプリケーションがダウンする可能性は高くなるだろう。一般的に、アップタイムにとっては、プロバイダーの数が少ない方が望ましい。加えて、賢明なのは、アプリケーションにある程度のレジリエンスを作り込んで、それほど重要でないサービスの可用性に対応できるようにすることだ(例えば、そうしたサービスとはデフォルトで非同期に通信するなど)。
ベンダーロックイン
ベンダーロックインについて過剰反応されることなしに、差異化したサービスについて語ることはできない。確かに、悲惨な事例はある。ITベンダーは頼りにされていることを利用して、ますます耐えられないレベルまで製品の価値を絞り出す。だが、ベンダーロックインを心配するなら、少なくとも、特定ベンダーからの移行が必要以上に難しいのか確かめるべきだ。
上記サービスフル・サーバーレスアプリケーションアーキテクチャの場合、書かずに済んだカスタムコードを書くことで、移行することができる。言い換えれば、その移行コストは、コード11,000行から36,500行になるだけだ。25,000行分書くのが非常に高くつくと思うなら(その通りだ。思い出そう、新しいコードは全て保守しなくてはならない!)、ベンダーに支払う額は妥当だろう。だが、もし自分でコードを書く方が安くつくと思うなら、そうしても構わない。APIは非常によく文書化されており、それらを使って動作するコードが(うまくいけば、そのコードをテストするコードも)すでにあるため、コードを書くのは難しくないだろう。
言い換えると、サービスフル・サーバーレスアーキテクチャを使うことで、自分の代わりにコードを書いてくれる別のチームを活用し、自分はコードを書かずに使用料を払うだけでよいということだ。ある時点で、使用ごと支払いが高すぎると感じたら、その機能を引き取って自分で書けばよい。これはデータが絶えず書き込まれ、永続化されるデータベースや仮想レイヤーのロックインとは違っている。これはステートレスなアプリケーションコードであり、はるかに入れ替えやすいものだ。
経験則
新しいアーキテクチャの概念を採用するとき、最も困難なことの1つは、自分が正しくやっているか把握することだ。そこで、アプリケーションが適切にサービスフル・サーバーレスのメリットを活かせているか考えるための3つの「経験則」を紹介する。
シックミドル層ではなく、シッククライアント
(上記のホテル/フライト検索の例のように)バックエンドで共通ロジックを持ちたくなるが、10年前に書いたアプリケーションの中間層にあったものを、サービス(例えば、画像処理のためのCloudinary、検索のためのAlgolia)やシッククライアント(例えば、Javascriptフレームワーク)に意図的にオフロードしよう。カスタムコードの大部分は、顧客とのインタラクションレイヤーであるべきだ。これは(上記例で言及したAPI統合のような)バックエンドコードを持たないことを意味しているわけでない。むしろ、エンドユーザーとのインタラクションを詳細にコントロールできることには大きな価値があるが、他から購入できるバックエンド機能を書くことにはほとんど価値がないということだ。
関数は相互に呼び出すものではなく、接着剤
上記のように、サーバーレスアーキテクチャ内で関数が関数を呼び出すのは、まずい判断だ。デバッグの難しさから、呼び出しのオーバーヘッド、無限ループによる愚かなコストまで、これら循環的複雑度の原因を取り除くことで、ずっとうまくいくようになる。カスタムコードの関数は、サービス間の接着剤であるべきだ。クライアント要求を受け取り、各種APIからデータを取り出し、それをコンパクトなデータ構造にまとめ、それをデータストアとクライアントの両方に送信する、といった具合だ。
カスタムコードではなく、カスタムリサーチ
サービスフル・サーバーレスアプリケーションを構築しているなら、全てを自分で書く場合よりも、リサーチにずっと多くの時間を費やす必要がある。アプリケーション機能のかなりの部分をサービスで実装しているためだ。あなたは適切なサービスを選択しているか検証しなくてはならない。また、サービスをアプリケーションと統合する適切な方法を見つけ出さなくてはならない。そのため、1、2時間かけて利用するパッケージを探すのではなく、何日も何週間もかけて概念実証コードを書き、様々なオプションをテストするべきだ。
別の言い方をすると、次の2つの式に表せる。
平均的に、10倍のコード行数は、10倍の技術的負債となる。つまり、ますます遅くて予測できない開発ベロシティと、平均的な開発者がうまく管理できないシステムになるということだ。
著者について
Joe Emison氏は、シリアルテクニカル共同創業者であり、最近3月に5番目の会社Branchを立ち上げている。これまで、BuildFax(DMGTが買収)、Spaceful(Xceligentが買収)、BluePrince(Harris Computerが買収)、EphPod(Wind Solutionsが買収)といったベンチャーを立ち上げた。加えて、ソフトウェア開発とクラウド移行に関して、DMGTポートフォリオの多くを含む多数の企業のコンサルタントを務めた。Williams Collegeから英語と数学の学位を、Yale Law Schoolから法律の学位を取得している。