re:Invent 2024: AWSがContainerとServerlessを比較 - 顧客事例から学ぶ選択基準
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2024 - Containers or serverless functions: A path for cloud-native success (SVS209)
この動画では、Cloud Nativeなプロダクトにおけるコンピューティングプラットフォームの選択について、ContainersとServerless Functionsの比較を通じて解説しています。AWSのSenior Solutions ArchitectのMaximilian SchellhornとWorldwide GTM Lead ServerlessのEmily Sheaが、運用性、統合性、移植性、価格の観点から両者の特徴を詳しく説明します。Delivery Hero、DVLA、Autodesk、Lexwareなど実際の顧客事例を通じて、Containerの完全なコントロール性とServerlessの運用負荷の低さ、そしてそれらを組み合わせたハイブリッドアプローチの有効性を示しています。特にAutodeskの事例では、建築シミュレーションの並列処理にLambda functionsを活用し、変動的な需要に対応している点が印象的です。
※ 画像をクリックすると、動画中の該当シーンに遷移します。
re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!
本編
Cloud Nativeアーキテクチャ設計の選択肢:ContainersとServerless Functions
皆さんの会社でCloud Nativeなプロダクトのアーキテクチャを設計することになったと想像してみてください。パフォーマンス、スケーラビリティ、信頼性、コストなど、必要な要件をすべて収集したとしましょう。その段階で、最適なコンピューティングプラットフォームは何かと考えることでしょう。アプリケーションをContainerとオーケストレーターで実行すべきか、それともServerless Functionsで実行できるのか? 本日は、皆さんがその道筋を進む上で、適切なツールを選択するためのガイドをご紹介したいと思います。トレードオフについてより深く理解していただくことを目指しています。それでは、SVS 209: 「ContainersかServerless Functions か - Cloud Nativeの成功への道」へようこそ。
私はAWSのSenior Solutions ArchitectのMaximilian Schellhornです。そして、Worldwide GTM Lead ServerlessのEmily Sheaと一緒に進行させていただきます。まず最初に、それぞれのテクノロジーを特徴づける基本的な要素について概観し、その後、運用性、統合性、移植性、価格の観点から評価していきます。そして最後に、実際の顧客事例を深掘りして、これまでに学んだことを実践的に見ていきたいと思います。
Containersの基本構造と運用特性
では、最初の候補であるContainersから始めましょう。 従来型の仮想マシンとは異なり、Containersはホストオペレーティングシステムのカーネルを共有するため、近年、Container分野では大きな進化とトレンドが見られています。簡単に言えば、従来の仮想マシンと比べて、1台のマシンにより多くのContainerを配置できるため、リソースの利用効率が向上し、起動時間も短縮されます。
Containerの構造を見てみると、いくつかのコンポーネントが関係していることがわかります。 まず、ContainerはContainer Imageによって定義され、複数のレイヤーを持っています。 Base Image、JavaやJavaScriptなどのRuntime、そして特定のポートでHTTP Webサーバーなどの機能を公開します。そのために時にはフレームワークを活用しますが、最も重要なのは私たちのアプリケーションコードです。
Containerそのものはどこかで実行される必要があり、それはCompute Nodeで行われます。そのCompute Node上には、Containerを移植可能にするソフトウェアであるContainer Runtimeがあります。あなたとわたしがContainer Runtimeを持っていれば、理想的な世界では、そのContainerは依存関係をすべて持っているため、交換が可能です。単一のContainerを実行するのは比較的簡単で、ローカルでDocker runを実行するだけです。しかし、本番環境ではより高度になります。高可用性のために同じContainerの複数インスタンスを実行したり、異なるContainerを実行したりする必要があります。そのため、これらを支援するContainer Orchestrationツールに依存することになります。
コンテナが不健全な状態になった場合、Container Orchestratorは必ず新しいコンテナインスタンスを起動します。Container Orchestratorはロードバランシングやポート・IPアドレスの管理などに依存しているため、そのようなシナリオでは通常、何らかのネットワーキングが必要になります。ライフサイクルの観点からは、Virtual Machineで見てきたものと非常によく似ています。まずコンテナイメージをプルして起動する初期化フェーズがあり、その後、通常は長時間実行される本番アプリケーションが動作します。特定の機能を公開したり、バックグラウンドジョブを実行したり、あるいはキューから常時データを取得するリスナーを持つこともあります。
これらすべてを本番環境に展開する方法は複数ありますが、最初に最もなじみ深いであろう、Server-basedコンテナについてお話しします。Server-basedコンテナでは、かなり多くのコンポーネントをセットアップします。ネットワーキング、VPC、Load Balancer、パブリックインターネットと通信するためのNAT Gateway、そして最も重要なのが、コンテナが最終的にデプロイされるコンピュートキャパシティとしてのAmazon EC2です。まず、コンテナイメージ、CPU、メモリ、場合によっては環境変数を指定したタスクまたはPod定義を作成し、Container OrchestratorがそのタスクやPodを実際のコンピュートノードにデプロイする処理を担当します。このOrchestatorの実装は変わることがありますが、定義を受け取って既存のコンピュートキャパシティに配置するという基本的な考え方は常に同じです。負荷が増加し、コンテナが特定のしきい値に達すると、コンテナやコンピュートノードが完全に使用されるまで、その特定のマシン上にさらにコンテナを追加していきます。
その後、Cluster Auto ScalingやKarpenterなどのツールを利用して、追加のコンテナに対応できる新たなコンピュートキャパシティを確保します。Karpenterはこの作業を非常にうまく行います。設定、可用性、価格に基づいて、コンテナを配置するための最適化されたコンピュートインスタンスを探します。
ここで重要なのは、スケーリングが使用率に基づいて行われるということです。追加のワークロードや追加のノード、コンテナをスケールアップするために、コンテナやマシンのCPUやメモリ使用率に基づいてしきい値を定義します。突然の負荷スパイクが発生した場合でも対応して追加キャパシティを作成できるように、100%の使用率は目指しません。
ネットワーキング、コンピュートノード、スケーリングなど、スタック全体を完全にコントロールできるこの環境では、ワークロード、チーム、メンテナンスプロセスもスケールする傾向にあります。アプリケーションだけでなく、インフラストラクチャコンポーネント、データベース、あるいはMessage Brokerのような追加コンポーネントも実行したくなるかもしれません。これがPlatform Engineeringという流れの出発点となりました。完全なコントロールが可能なため、その上に優れた抽象化レイヤーを構築し、チームに提供することができるのです。
これまで学んできたことをまとめますと、サーバーベースのContainerでは、私たちにとって馴染みのある環境と、選択可能な幅広いオープンソースプロジェクトがあります。スタック全体に対する完全なコントロールとデバッグ機能を持ち、どのようなインスタンスタイプを実行するかを決定できるというインフラの柔軟性も備えています。しかし、その一方で、アップグレード、メンテナンス、CVEパッチ適用などを含むクラスター運用の責任も負うことになります。確かに、Managed Node GroupsやAutoモードを通じてその責任を軽減する方法はすでにありますが、さらに一歩進めることができるかもしれません。
Serverlessの本質と様々な実装形態
運用の負担をさらに減らすことはできないでしょうか?ここでServerlessの話に移りたいと思います。ここで少し用語について触れておきたいと思います。というのも、多くの人々がこれを誤解していることをよく目にするからです。「ContainerかServerlessか」といった派手な見出しをよく目にしますが、Serverlessとは実際には何を意味するのでしょうか?その本質は、インフラを管理することなくアプリケーションやサービスを構築・実行する方法です。まず、私たちは皆、Serverlessの裏側には実際にサーバーが存在することを知っているでしょう。こちらは私たちのインフラストラクチャデリバリーチームのDelanです - 皆さんはサーバーを気にする必要はありませんが、Delanにはまだかなりの仕事が残っているんです。
さらに、Serverlessは人によって様々な意味を持ちます。ある人にとってはゼロへのスケール、他の人にとってはメンテナンスやアップグレードが不要であることや、急速なスケーリングを意味するかもしれません。しかし最も重要な点は、Serverlessが何でないかということです。Serverlessは特定の技術の同義語ではありません - ServerlessはAWS LambdaやFunction as a Serviceと同じではありません。Serverlessは関数以上のものです - Amazon S3、Amazon API Gateway、Amazon SQSなどを考えてみてください。私たちの例に戻ると、AWS Fargateを使用してContainerをよりServerlessな方式で実行することもできます。Fargateは、高可用性で最新のインフラストラクチャ上でContainerを実行できるServerlessコンピューティングエンジンです。
私たちの例を見てみると、ネットワーキングスタックとContainer orchestrationを活用することができます。しかし今度は、Containerは最新のランタイムを使用する新しくプロビジョニングされたFargateホスト上に配置され、シングルユースかつシングルテナントのコンピュートインスタンス上で実行され、仮想化の境界によって分離されています。また、ワークロードの自動スケーリングも支援してくれます。追加のインスタンスを用意する必要がないため、Cluster Auto Scalerのようなものに頼る必要はありません - これはFargateが私たちに代わって処理してくれます。これを比較してみると、異なる抽象化レイヤーが見えてきます - 左側では完全なコントロールがあり、Fargateでは、その責任の一部をすでに軽減することができます。そして、AWS App Runnerを考えると、さらに一歩進めることができます。
AWS App Runnerでは、HTTPサーバーを直接公開できるため、ネットワーキングスタックやロードバランシングについて考える必要がありません。ここで、サーバーベースのContainerとServerless Containerがあるのだから、これで全部揃ったのではないか、Containerだけで何でも構築できるのではないか、Containerはコンピューティングインフラストラクチャのすべての答えなのだろうか、と疑問に思うかもしれません。私の考えでは、ここにはまだ2つの最適化できる領域があると思います。
最初の最適化領域はスケーリングの観点からのものです。私たちはまだ大きなバケットを扱っており、マシンやコンテナを60%、70%、80%といった使用率に基づいて扱っています。スケーリングのために一定のしきい値を確保しておく必要があるため、100%の使用率になることはありません。つまり、ここには改善の余地があるということです。2つ目の領域は、長時間実行という性質に関するものです。マシン、コンテナ、ロードバランサーがあり、特定のタスクのシャットダウンを自動化することはできますが、より長い期間それを公開しておきたい、あるいは少なくともコンテナオーケストレーターがより長期間実行され続けるという状況があります。この点について何か改善できるかもしれません。これら2つのシナリオで、Serverless Functionsが役立つかどうかを見ていきましょう。
AWS Lambda Functionsの特徴と実装方法
ここでMaxの続きを受けて、今日の方程式の後半である、特にAWS Lambda Functions 、つまりServerlessの世界とその特徴について説明していきます。先ほど学んだコンテナとは異なる特有の特徴についてお話しします。Lambda Functionの基本的な構造について説明させていただきます。Lambda Functionを書こうとして考えるとき 、主に3つの主要なコンポーネントについて考える必要があります。
まず最初に、Function Handlerがあります。これは関数が呼び出されるたびに実行されるコードの部分です。お好みのプログラミング言語で書くことができ、Function Handlerは入力引数と、オプションのコンテキストを受け取ることができます。この例では、コンテキストからユニークなリクエストIDを取り出してログに記録する非常にシンプルな関数があります。関数コードはどこかで実行される必要があります。コンテナの場合はCompute Nodesについて話しましたが、関数の場合はFirecracker Micro VMを使用します。これはAWS Lambdaによって管理され、ベアメタルEC2上で実行されます。サーバーや基盤となるインフラストラクチャを管理する必要はありません。すべてLambdaサービスによってカバーされています。
Lambda Functionsについて理解しておくべき重要な点は、これまでの開発方法とは異なり、関数コードを直接呼び出すわけではないということです。Micro VM上でHTTPリスナーを作成したり、ポートを開いたりすることはありません。代わりに、Lambdaサービスが中間に位置し、その関数コードを呼び出します。Lambdaサービス はAPIエンドポイントやLambda Invoke API、そして次のスライドで説明する様々なサービスからリクエストを受け取ることができます。Lambdaを呼び出すと、入力を受け取り、関数コードに渡し、関数コードの実行後に返された結果を返します。
Lambdaサービスはいくつかの重要な機能を提供します。まず、すべてのインフラストラクチャを管理するため、考慮したり、パッチを当てたり、運用や保守したりする必要のあるサーバーやクラスターは存在しません。すべてLambdaが処理します。もう1つの特に強力な機能は 、すべてのスケーリングを処理することです。受信リクエストを処理するために必要な関数のキャパシティはすべてLambdaによって管理されます。これは特に、Event-Driven Architectureや、他のサービスから入ってくるイベントやメッセージに応答するLambda Functionsを書く際に非常に強力な機能となります。
もう少し詳しく見ていきましょう。 こちらは、Lambdaで構築する非常に一般的なイベント統合の例です:Amazon SQSキューからメッセージを読み取って処理する場合です。コンテナ上で構築する場合や、VMで実行するコードを書く場合、新しいメッセージを常にチェックするポーラーを書く必要があるかもしれません。しかし、Lambdaのコンテキストでは、Lambda Event Source Mappingを利用することができます。これは、Amazon SQSのようなアップストリームサービスとの特に強力なダイレクト統合で、Lambdaにキューからのポーリングを任せ、そこにあるメッセージを取得することができます。
このアプローチにより、メッセージを処理して抽出し、それを渡すために自分で書く必要のあるコードの量が大幅に削減されます。Event Source Mappingは、さらに素晴らしい機能を提供します - キューの深さなどを認識し、読み取り待ちのメッセージ量に基づいて必要に応じて追加のキャパシティをスケールアップすることができます。 これらのEvent Source Mappingは、Amazon SQS、Amazon MQ for RabbitMQとActiveMQ、そしてDynamoDB Streams、KinesisやKafkaなどのサービスで利用可能です。この2週間で、KafkaのEvent Source Mappingに対してProvision Modeという新機能をリリースしました。自己管理型のKafkaやMSK for Kafkaを実行していて、アプリケーションに非常に厳格なパフォーマンス要件がある場合、このProvision Modeを使用することで、Lambdaのイベントポーラーに必要な容量を確保することができるようになりました。
スケーリングについて少し話してきましたが、これはLambdaの特に独特な特徴の一つだと思います。 ゼロまでスケールダウンできる機能は注目に値しますが、そのスケーリング内には、視覚化して理解しておくと役立つ特定の動作があります。例えば、先ほど話していた関数コードを例に取ると - リクエストが来ない場合やAPIエンドポイントにトラフィックが発生していない場合、何も実行されていません。リクエストを待機するために常にアイドル状態で待機している何かがあるわけではなく、ゼロにスケールされています。 最初のリクエストが来ると、初期化期間が発生します。この初期化期間には、コードのダウンロード、ランタイムの起動、そして関数コードを実行する準備が整った実行環境を立ち上げるために必要な最初のステップが含まれます。
これはコールドスタート期間と呼ばれることがあり、 新しい実行環境を起動する際や最初のリクエストを処理する際に必ず発生します。Lambdaサービスチームは、Lambdaが最初にローンチされてから(今年で10周年を迎えます)、長年にわたって多くの改善を行ってきました。コールドスタート時間はLambdaサービス全体で短縮され、さらにSnapStartのような新しいテクノロジーや機能をリリースして、コールドスタート時間をさらに短縮しています。コールドスタート時間が最も長かったJavaに対してSnapStartをリリースし、実行環境のスナップショットを取得して再利用することを可能にしました。これにより、Javaのコールドスタート時間が10分の1に短縮されました。この2週間で、PythonとNETに対してもSnapStart機能をリリースしました。
コールドスタート後の実行は、 ウォームスタートとなり、実行環境がすでに起動して実行中なので、すぐにコードの実行を開始できます。考慮すべき興味深い点は、実行中に別のリクエストが来た場合の動作です。この場合、Lambdaはキャパシティのスケールアップを開始し、 別の実行環境が初期化期間とコールドスタートとともにスピンアップします。これにより追加のキャパシティが得られ、リクエストを並列で処理できるようになり、Lambdaはリクエスト量に応じて必要なだけこれらをスピンアップし続けます。実行に関する重要な注意点として、実行時間の制限が15分であることが挙げられます。 実行しようとしているプロセスが15分以上かかる場合は、15分未満の小さなチャンクに分割する方法を検討するか、より長時間実行するプロセスの場合はコンテナでの実行が適しているかもしれません。
しばらくすると、実行やリクエストが発生し始めますが、時間とともにリクエスト量が減少する可能性があります。そうなると、実行環境が待機状態のまま、処理すべきリクエストがない状態になります。 一定時間が経過すると、その実行環境はシャットダウンし、スケールダウンが始まります。リクエストがまったくない場合は、ゼロにまでスケールダウンする可能性もあります。実行環境は一時的なものなので、メモリや一時ストレージにデータを少し保存することはできますが、長期的にデータを保持したい場合は、後でアクセスできるように DynamoDB データベースなどの外部ストレージに保存することが非常に重要です。まとめると、Lambda のスケーリング動作により、リクエストごとのきめ細かなスケーリングと、マネージドな自動スケーリングが可能になります。
ContainersとServerless Functionsの比較:運用性、統合性、移植性、価格設定
コンテナとサーバーレス関数では、構築方法や対応するワークユニットのタイプに大きな違いがあります。 最も重要な違いは、コンテナが常に実行され、利用可能な状態にあるという点です。処理するものが何もない待機状態であっても、多くのリクエストやワークユニットを処理している状態であっても、コンテナは存続し続けます。一方、サーバーレス関数は、そのワークユニットを処理するためだけに存在し、リクエストがない場合は関数は実行されません。これが、コンテナとサーバーレス関数を使用する際に気付く根本的な違いです。
ここで疑問に思われるかもしれないのは、アプリケーション内のすべてのワークユニットに対して、個別の AWS Lambda 関数が必要かどうかということです。アプリケーションの一部としてコードをどのように整理し、Lambda 関数に分割するかについて説明しましょう。 例えば、様々なメソッドを持つ API エンドポイントを構築する場合、GET メソッドには GET ハンドラー、POST メソッドには POST ハンドラーがあるかもしれません。これらを2つの別々の Lambda 関数に分割してコードを分離することができます。この方法は上手く機能しますが、多くの API エンドポイントとメソッドがある場合、何百もの関数が必要になり、複雑さが増すのではないかと心配になるかもしれません。
しかし、このアプローチを完全に踏襲する必要はありません。より単純なアプローチとして、 これらの API エンドポイントごとに1つの Lambda 関数を用意し、関数コード内で異なるメソッドへのルーティングを行うことができます。これらのアプローチを選択する際には、様々な要因を考慮する必要があります。同じ開発ワークフローで変更されるコードは一緒に保持するなど、標準的なソフトウェア設計のベストプラクティスを考慮してください。また、Cold Start の最適化も検討事項です。例えば、ユーザーを作成してから更新するというプロセスがある場合、これらをまとめてパッケージ化することで、2回の Cold Start を避けることができます。スケーリング動作も考慮に入れてください。アプリケーションが多くの GET リクエストを受け取るが POST リクエストは少ない場合、必要に応じてスケールできるように分割することもできます。さらに、権限境界とセキュリティの細かい範囲設定も考慮できます。例えば、注文データベースと請求書データベースなど、異なるデータベースにアクセスする必要があるコードを異なる関数に分離するといった具合です。
コンテナとサーバーレス関数の基本を確認したところで、Max が様々なユースケースに対してこれらの異なるテクノロジーを適用し、決定を下すためのガイドを提供します。 運用、統合、移植性、価格設定といった異なる観点から、それぞれの分野における主要な強みを特定していきます。 まず運用面から見ていくと、幅広いスペクトルがあることがわかります。左側には、スタック全体、スケーリング、ネットワーキング、インスタンス選択の完全なコントロールがあります。AWS Fargate を使用することで、プロビジョニングや自動スケーリングの処理など、その一部を削減できることについて説明しました。関数を使用すると、ランタイムが管理されるため、さらに踏み込んだ対応が可能です。Java、JavaScript、Python などの Lambda マネージドランタイムを使用すれば、自動的に更新されます。配置に関しては、Lambda サービスが手動設定なしで複数のアベイラビリティーゾーンで関数を実行することを保証します。
運用について考えるとき、それは制御だけでなく柔軟性とシンプルさも重要です。そしてコンテナは優れた柔軟性を提供します。私たちには幅広いハードウェアやツールの選択肢があります。WindowsやLinux上で実行でき、特定のインスタンスタイプを選択し、RAMの要件を決定し、HDDドライブを設定し、ストレージ容量を指定し、さらには機械学習ワークロード用のGPUも含めることができます。また、デプロイメントの柔軟性も得られます。
コンテナランタイムのおかげで、コンテナはクラウド、ローカル、オンプレミスで実行できます。一方、Functionsはシンプルさを提供します。先ほど話したランタイムのサポートがすでにあり、メモリ構成を選択すると、CPUとネットワーク帯域幅が比例して割り当てられます。アーキテクチャ(X86やARM)を選択できますが、それ以外は提供されたメモリに基づいて比例配分されます。ローカル開発では、AWSなどのフレームワークやツールを使用してFunction環境をローカルでエミュレートし、ローカルな開発体験を得ることができます。
この柔軟性からシンプルさまでのスペクトラムは、運用だけでなく統合にも当てはまります。コンテナ、特にKubernetesエコシステムでは、以前見たように、選択するツールについて非常に柔軟です。KAやCrossplaneなど、活用できる様々なプロジェクトがあります。大きなコミュニティと豊富なドキュメント、そして関連する講演があり、ここでの選択の自由度は非常に高いです。さらに、実行するアプリケーションについても柔軟で、任意のポートや機能を公開できます。以前話したようにデータベースやメッセージブローカーを公開したり、Kafkaクラスター全体を公開したり、オープンソースのオペレーターを活用してその運用をさらにシンプルにし、それを中心にFunctionを使ってプラットフォーム全体を構築することができます。
一方で、AWS エコシステムとの統合は非常に優れています。例えば、オブジェクトや画像がAmazon S3にアップロードされた場合、AWS Lambda functionをトリガーすることができます。AWS Step Functionsワークフローの特定のステップでトリガーしたり、Amazon EventBridge Schedulerでスケジュールに基づいて10分ごとにFunctionコードを実行したりすることもできます。コード自体に統合を書く必要なく、サービスを簡単に連携させることができます。以前にメッセージングの面でも見たように、Amazon SQSからの取得や、Kafkaからの読み取り、Amazon DynamoDBテーブルのチェンジストリームからの読み取りを、ポーリングロジックを書くことなく自動的に行うことができます。
統合における素晴らしい機会がある一方で、優れた統合を持つことがポータビリティの低下というトレードオフを伴うのではないかと考えることがあります。これについて、2つの異なる視点から一緒に探っていきましょう。左側にコンテナ、右側にFunctionを置いて、インバウンド通信、アウトバウンド通信、パッケージング、オーケストレーションについて見ていきます。コンテナ側では非常に柔軟で、機能を公開できます。つまり、HTTPリクエスト、gRPC、WebSocket、プレーンなTCPポートを扱うことができます。アウトバウンド通信では、ネットワーク内のものや、ファイルシステム、特定のデータベースと通信できます。また、その定義を構成するコンテナイメージのさまざまな部分や、それをスケールするためのコンテナオーケストレーションについてもすでに説明しました。
AWS Lambda側では、Lambdaは様々なサービスによって呼び出され、受信する通信はJSONペイロードのような構造になっています。このスキーマは、実際に関数を呼び出すサービスによって定義されます。送信通信に関しては、VPC対応のLambda関数を使ってネットワーク内のリソースと通信したり、Amazon DynamoDB、リレーショナルデータベース、Amazon EFSのようなファイルシステムやローカルの一時ストレージと通信したりできます。パッケージングについては、関数コードをZIPアーカイブとしてバンドルすることも、コンテナイメージとしてバンドルすることもできます。これについては後ほど詳しく説明します。また、実際のコードとLambdaサービスの間のリンクを確立するために、Lambda Runtime APIと統合する必要があります。ここで、ポータビリティに関して最も重要な側面に注目し、コンテナ側を見てみると、オーケストレーションについていくつかの考慮事項があることがわかります。
オーケストレーションの側面について詳しく説明させていただきます。コンテナ自体は全ての依存関係を持ち、簡単に移植できますが、それはコンテナオーケストレーターには当てはまりません。オーケストレーターはロードバランサー、CNS、特定のストレージドライバーに依存しています。そのため、コンテナの移植は簡単かもしれませんが、オーケストレーター全体の移植にはより多くの作業と注意が必要になります。
関数側のポータビリティについて考える際、まずパッケージングから始めたいと思います。関数コードをZIPアーカイブにパッケージ化して、Lambdaのマネージドランタイムに渡すことができます。もしコンテナの分野に精通している、あるいはそこから来た方であれば、既存のコンテナツールを活用して関数をコンテナイメージとしてパッケージ化することもできます。Amazon ECRコンテナレジストリのような既存のツールを活用できます。追加のコンポーネントを加える必要なく、Lambda Runtime APIへの接続を確立する公式のAWS Lambdaベースイメージを使用できます。ここでPythonを使用した簡単な例を見てみましょう。requirements.txtをコピーしてインストールし、関数コードをコピーして、Lambdaサービスが実際の関数を呼び出す場所を知るためのエントリーポイントを提供します。
また、会社が特定のポリシーを持っている場合や、独自のイメージを使用したい場合は、カスタムイメージを使用することもできます。ただし、Lambda Runtime APIとの通信を確立するために提供しているライブラリであるRuntime Interface Clientとの互換性を確保する必要があります。さて、話を戻すと、ベースイメージを使用したり、ZIPバンドリングを使用したり、マネージドランタイムにデプロイしたりする場合、通常はLambda Runtime APIを直接扱う必要はありません。最後に説明したいのはJSONペイロードについてです。Lambda関数には単純なHTTPリクエストは届きません。API Gatewayが関数を呼び出す際、単純なHTTPリクエストとは異なる特定のJSONペイロードを受け取ります。
これを互換性のあるものにできないかと思われるかもしれませんが、それを支援するフレームワークやアダプターが存在します。AWS Lambda Web AdapterとJava用のServerless Java Containerという2つのプロジェクトがあり、これらが変換を行います。これらをライブラリとして関数に追加すると、JSONペイロードを受け取って、既存のフレームワークが理解できる形式に変換します。例えば、Spring BootやExpress.jsでパスベースのルーティングに慣れている場合、アダプターが変換を処理してくれるため、そのまま使い続けることができます。これは特に特定のHTTPベースのアプリケーションに役立ちます。
追加のツールを必要とせず、コードの抽象化だけで実現できる代替案もあります。もしよろしければ、一緒にJavaで簡単な関数を書いてみましょう。他のプログラミング言語に詳しい方でも大丈夫です。コードを読みやすく抽象化してありますので。例えば、API GatewayからHTTPリクエストを受け取り、そのヘッダーをDynamoDBテーブルに書き込む簡単な関数を作ってみましょう。ここではDynamoDBクライアントを定義し、先ほど見たような関数を書いていきます。
コード移植性の向上:抽象化とインターフェースの活用
handleRequestメソッドは、API Gatewayリクエストを入力として受け取り、API Gatewayレスポンスを返します。ここでは、後で内容を設定するレスポンスを定義します。API Gatewayのヘッダーに複雑なビジネスロジックを適用した後、DynamoDBのパーティションキーとなるアイテムを作成し、先ほどの計算結果を値として設定します。そして、DynamoDBクライアントを使用してDynamoDBテーブルに書き込みます。私たちは良き開発者として、エラーケースも考慮し、エラーをログに記録して適切なステータスコードとともにAPI Gatewayレスポンスを返します。
さて、このコードは簡単に移植できると思いますか?そうは思えませんよね。API GatewayをApplication Load Balancerに置き換えたり、DynamoDBを別のデータベースインスタンスに置き換えたりすることは簡単にはできません。基本的に、このコードの各行が特定の実装に依存しているのです。では、同じ機能を少し異なるアプローチで書き直してみましょう。今回は、直接的な実装の代わりに、インターフェース、データリポジトリ、そしてファクトリーによる決定を使用します。
例えば、DynamoDBに関する具体的な実装をすべて含むDynamoDBアダプターを作成できます。必要に応じて、後からこの実装を切り替えることができます。私のコアビジネスロジックは、もはやAPI Gatewayリクエストに直接依存しません。代わりに、独自の入力やリクエストオブジェクトを定義し、API Gatewayに関連するすべてのコンポーネントは、API Gatewayアダプターとして分離されます。このアダプターは、変換されたヘッダー関数を呼び出すだけです。ご覧のように、黄色い部分、つまり実装に直接紐づいているものがすべて抽象化されています。この抽象化により、後から実装を変更することができます。つまり、API Gatewayアダプターの代わりに、従来のHTTPアダプターを使用することもできます。
ビジネスロジックの核心部分は常に同じままです。この柔軟性により、実際に多くのお客様が同じコードを異なるコンピューティングプラットフォームで活用しているのを見てきました。これは、注文アイテムや複雑な計算のリストなどのアイテムを処理するシナリオです。これらの計算の中には、Lambdaの制限である15分以上かかるものもあります。その場合、キューに書き込んでコンテナフリートで処理します。一方で、迅速に処理する必要がある小規模な計算も多数あります。そのため、そのようなユースケースではLambdaのスケーリング特性を活用したいと考えます。Amazon EventBridgeのようなルーティング層を使用して、メッセージの特性に基づいてどのコンピューティングプラットフォームを使用するかを決定することができます。
同じビジネスロジックでありながら、スケーリングの仕組みやコスト要件に応じて異なるコンピュートプラットフォームを選択するという観察から、ContainerやFunctionの選択は主に非機能要件によって決定されることがわかります。 Web APIだからContainerを使うべき、特定のアプリケーションだからFunctionを使うべきといった単純な判断はできません。スケーリングパターンやオペレーション、その他の要因を十分に考慮して判断する必要があります。その判断における重要な要因の1つが価格設定です。まずはContainerの観点から見ていきましょう。
Containerの価格設定は、 Containerそのものやその基盤となるコンピュートノードといったリソースに基づいています。課金の単位は秒単位から分単位です。これを視覚化すると、リクエストを受け付けている実行中のContainerがあり、例えば60%の使用率といったスケーリングのしきい値を設定します。このしきい値を超えると、追加の負荷に対応するために新しいContainerが初期化されます。負荷が減少すると、追加のContainerはシャットダウンされ、最初のContainerだけが稼働し続けます。これを線で表すと、実際のスケーリングと価格設定は使用率に直接連動していないことがわかります。リクエスト数に関係なく、常にContainer全体の容量分を支払う必要があります。
これは、高負荷のシナリオや一定で予測可能な負荷など、使用率が非常に良好な場合に特に効率的です。使用率についての知識が豊富であればあるほど、実際の個々のリクエストあたりのコストを抑えることができます。負荷や使用率について把握できている場合は、Reserved InstanceやSavings Planなどの1年単位のコミットメントを行うことで、さらにコスト効率を高めることができます。また、インフラを管理するための専任チームが必要になる可能性もあるため、運用コストも考慮に入れる必要があります。一方、 Lambdaを見てみると、リソースベースの価格設定とは対照的に、ミリ秒単位の細かい粒度で使用量に基づいて課金されます。非常に細かい粒度で、需要に応じて自動的にスケールします。需要が全くない場合は、ゼロまでスケールダウンするため、グローバルな実験を行う際には特に効果的です。インフラコストを非常に低く抑えることができるからです。
これは、市場投入までの時間を短縮し、戦略をテストするための実験フェーズで非常に効率的です。FunctionをSingaporeやFrankfurtにデプロイしても、誰も使用していなければ料金は発生しません。
高負荷で予測可能な一定の負荷にはContainer、変動的またはスパイク的なワークロードにはFunctionが適しています。データに基づいた判断を行うことをお勧めします。まだ負荷パターンがわからない場合は、より柔軟なアプローチから始めるのがよいでしょう。後でデータを得て使用率がわかれば、十分な情報に基づいて判断し、Containerに切り替えることができます。あるいは、使用率を正確に把握していて、Containerで変動する需要がある場合は、Functionが適しているかもしれません。まずデータを取得し、その後どちらのコンピュートプラットフォームが適しているかを判断してください。
この考え方は、自家用車とカーシェアリングの比較という実生活の例からも理解できます。カーシェアリングアプリを使えば、好きなだけ車を借りることができ、1時間や1週間、あるいは1日の特定の時間帯だけ使用する場合には非常に効率的です。車が故障しても誰かが対処してくれるので心配する必要もありません。しかし、1週間や1ヶ月間、24時間体制でカーシェアリングを利用すると、その柔軟性すべてが必要ないにもかかわらず、かなり高額になる可能性があります。そのような場合は、自分の車を所有して予測可能性を確保する方が効率的なモデルかもしれません。
この基本的な判断基準を理解したところで、実際の顧客事例を見ながら、どのような判断がなされたのかを見ていきましょう。 興味深いのは、これらの顧客がそれぞれの状況に応じて異なる判断を下していることです。完全にContainerを採用し、Platform Engineeringのアプローチを取る顧客もいれば、イベント駆動型のスパイクのある処理に対してAWS Lambdaとそのサービスを全面的に活用する顧客もいます。さらに興味深いことに、両方を組み合わせている顧客もいます。アプリケーションの一部がContainerに適している場合や、Functionsに適している場合、あるいはモダナイゼーションの進行状況によって使い分けているケースもあります。
実例に学ぶ:Delivery Hero、DVLA、Autodesk、Lexwareの選択
それでは、具体的な顧客事例を見ていきましょう。最初の事例は、ベルリンを拠点とし、世界最大級のフードデリバリーネットワークを運営しているDelivery Heroです。 Delivery Heroには多くの子会社があり、それらの子会社全体で、アプリケーションを実行するためのインフラストラクチャやツールに関して同様の要件があることがわかりました。Delivery Heroは、すべての子会社の開発者がアプリケーションを作成、デプロイ、運用する方法を標準化するためのContainerプラットフォームを構築するアプローチを選択しました。彼らはContainerを採用し、Amazon EKSで構築することを選択し、300以上のEKSクラスターと15,000のコンピュートノードを持つプラットフォームで、1日1,000万件の注文を処理しています。Argo CDやKarpenterなどのContainerエコシステムのツールを使用し、AWS Controllers for Kubernetesを使用してKubernetesネイティブな方法でAWSサービスをデプロイしています。これは、Platform Engineeringのアプローチを取り、約200人のPlatform Engineerがこのプラットフォームで作業し、その上でアプリケーションを構築・デプロイする3,000人以上の開発者に抽象化とブループリントを提供している顧客の好例です。
次は、英国のDriver and Vehicle Licensing Agency(DVLA)です。DVLAは英国政府の一部で、英国の運転免許証と車両の全国登録簿を管理しています。最近、彼らは運転免許サービスアプリケーションの大規模なモダナイゼーションに着手しました。
このモダナイゼーションには、フロントエンドとプレゼンテーション層の完全な更新と改善が含まれていますが、クラウドを最大限に活用し、アプリケーションを継続的に進化させていくために、バックエンドの大幅なモダナイゼーションとリファクタリングも行われています。彼らはアーキテクチャ内で最も適切な場所で、両方のアプローチを採用しています。
まず、彼らはKubernetesとAmazon EKSを使用したContainersプラットフォームを持っています。 彼らが行ったのは、必要なアプリケーションタイプに対して非常に標準的なコンポーネントを構築したことです。Ruby on RailsでUIを実行するための標準コンポーネントがあり、Javaのための標準コンポーネントも用意しています。安定性が高く成熟したJavaスタックをコンテナとして実行しており、100以上のJavaコンポーネントとライブラリを使用し、データベースアクセスのためのコネクションプーリングなどのJavaのベストプラクティスを活用できます。100人以上の開発者がこのContainersプラットフォームを利用して、セルフサービスで自分たちのアプリケーションを構築しています。
運転免許証申請処理のバックエンドに関して、このワークロードは非常にイベント駆動型で非同期処理に最適化されていたため、Serverlessが非常に適していることに気づきました。 このアプリケーションのエンドツーエンドのフローを説明すると、新しい運転免許証の申請をしようとしてウェブサイトにアクセスした場合、UIを通じてフロントエンドでリクエストを行い、その運転免許証申請のためのリクエストと情報がバックエンドに届きます。申請には当然、申請者に関する多くの詳細情報がありますが、もう一つ重要な要素は運転免許証に使用する写真です。
サイズやポスト処理に関するチェックが必要ですが、特に重要なのは、重要な文書に使用される写真として適切かどうかを確認することです。写真が鮮明で、例えば猫の写真ではないことなど、公式文書に使用する前に確認したい項目があります。このプロセスではAWS Step Functionsワークフローとそれらのチェックを実行するAWS Lambda関数を使用しています。Amazon Rekognitionなどのサービスを使用してAI/MLで画像の内容を初期チェックしていますが、最終承認は人間が行う必要があります。
この処理が非同期処理に特に適している理由は、人間が数分で承認の準備ができている場合もあれば、週末に提出されたフォームの承認に数日かかる場合もあるためです。Step Functionsワークフローはコールバックトークンの待機ステップを使用し、人間の承認を待つ間はアイドル状態で何も実行せずにワークフローを一時停止します。コールバックトークンが返ってくると、ワークフローが再開され、CIコンポーネントが処理を続行します。これは、非常にモダンな新しいアプリケーションを構築し、異なるユースケースに対してContainersプラットフォームとServerlessの長所を活用した素晴らしい事例です。
次に紹介したい顧客はAutodeskです。Autodeskは、建築家やデザイナーが環境や気候が彼らの設計とどのように相互作用するかを理解するためのシミュレーションを実行できる、Autodesk Formatという新製品を開発していました。彼らは反復的に開発を進め、ほぼリアルタイムでシミュレーションを実行して洞察を得られるようにしたいと考えていました。最初はKubernetesでこれを試みましたが、ワークロードのいくつかの特性に課題を感じました。まず、シミュレーションは非常にリソース集約的で、建築家がいつシミュレーションを必要とするか予測できないため、需要が変動的でした。Kubernetesは必要なスピードでのスケールアップが難しく、また、コアクラスターの運用と保守に関する追加作業は、彼らが関わりたくないオーバーヘッドでした。
代わりに、彼らはサーバーレスとAWS Lambda関数ベースのアプローチを選択しました。これが彼らのシミュレーション実行のワークフローです。Lambda関数が新しい分析をトリガーし、入力を準備し、そのジョブを分割して多数のLambda関数で並列実行し、その後結果を収集して統合します。これらすべてが非常に迅速にスケールアップし、並列処理の量に応じて素早く処理することができます。このケースのもう一つの興味深い点は、建物への日射照度の光線追跡にGPUを必要とするシミュレーションがあったことです。LambdaはGPUをサポートしていませんが、Amazon ECSはサポートしているため、GPUが必要なシミュレーションの場合はECSで実行するように切り替えることができました。最後の顧客事例として、Lexware Officeについてお話しします。
Lexwareはドイツの企業で、SaaS型の会計ソフトウェアを提供しています。当初、このプロダクトはオンプレミスで運用されていましたが、ボトルネックやパフォーマンスの問題に直面していました。そこで、クラウドへの移行を決定しました。最初はアプリケーションをそのままリフト&シフトしただけでしたが、それだけでもボトルネックとパフォーマンスの問題を軽減し、9万人の新規ユーザーをアプリケーションに受け入れることができました。
その後、さらなるモダナイゼーションを進めることにしました。アプリケーションの一部をコンテナ化し、さらにイベント駆動で負荷の変動が大きい部分についてはサーバーレス関数の活用を検討しました。サーバーレス関数の活用例として、SaaSプロダクトで請求書の支払いが完了した際に、ユーザーが設定したHTTPエンドポイント(CRMやERPなどの外部システム)に通知を送信する機能があります。
負荷パターンを見ると、これがサーバーレスに適していると判断した理由がわかります。下のグラフに示されているように、請求書の支払いは通常月1回程度か特定のスケジュールで行われるため、非常にスパイク的なワークロードとなります。1分あたり8,000リクエストまで急上昇し、その後ゼロまで落ち込むため、常時稼働する容量を用意することは合理的ではありませんでした。新しいイベントが生成されるか、新しい請求書の支払いに関するメッセージがキューに入ると、Node.jsで書かれた128メガバイトの小さなLambda関数が動作します。このWebhook関数はメッセージを取得し、DynamoDBテーブルでユーザーが設定したHTTPエンドポイントを検索します。
その後、該当するエンドポイントにコールを行い、これらの小さなLambda関数がイベント駆動でスケールアップとスケールダウンを行います。これは、お客様がアプリケーションをリファクタリングしてモダナイズする方法の良い例です。コンテナとサーバーレス関数を組み合わせて使用し、将来のユースケースに向けて継続的にモダナイズを進めているのです。
ContainersとServerless Functionsの主な違いと選択基準
ContainerとServerless Functionの主な違いについて考えてみましょう。まず最初に考慮すべき特徴は、ライフサイクルとロードプロファイルです。Containerはインスタンス化されると常時稼働し、終了されるまで複数の作業単位を処理できます。これは、使用量が安定していて予測可能なワークロードに適しています。一方、Lambda Functionはリクエストがあった時にのみ起動し、処理要求があるときだけ実行され、大規模なスケールアップが可能で、必要がなければゼロまでスケールダウンします。これは変動の大きい不規則なユースケースに最適です。
次に運用面についてです。Containerでは、より高度な制御と柔軟な設定オプションが得られますが、それに伴って相応の責任も発生します。Serverless Containerのようなオプションを利用することで、その負担の一部を軽減することはできますが、自身で管理する必要があることは確かです。一方、Serverless Functionの場合は、AWSがより多くの運用を担当してくれるため、より完全に管理された高可用性のサービスを活用できます。
統合の観点では、Containerは幅広いオープンソースのエコシステムから選択できる利点があります。一方、Functionの場合は、Lambda FunctionとAWSの他のサービスを接続するための最適化された標準統合機能が多数用意されています。先ほど説明したアップストリームのイベントソースに対するイベントソースマッピングのような機能は、Lambdaに組み込まれており、自分でコードを書く必要がありません。
移植性に関して、Containerの場合、アプリケーション層では実行中のコードをクラウドやオンプレミス、必要な場所にそのまま移行できるため非常にシンプルです。ただし、インフラストラクチャ層では、別の環境に移行する際に変更が必要な部分が出てくる可能性があります。Function側では、Container イメージやフレームワーク、Hexagonal Architectureを活用してLambda Functionをデプロイすることで、実装の詳細を抽象化し、ビジネスロジックだけを分離できます。これにより、時間の経過とともに変更が必要になった場合でも、Function コードの移植性を高く保つことができます。
最後に価格設定についてです。Containerは常時稼働しているため、リソースやコンピュートノードベースの比較的粗い粒度の価格設定になります。一方、Functionの場合は、使用量に密接に連動した非常に細かい粒度の価格設定となり、リクエスト量の変化に応じて料金が変動します。
このセッションにご参加いただいた皆様、多くの学びを得ていただけたことと思います。また、他にもいくつか興味深いセッションがございます。本日後ほど「Kubernetes meets Serverless: Deploy EDAs with Kubernetes」セッションでお会いできますし、Serverlessアプリケーションのコスト modeling に関する素晴らしいセッションや、Lambda と ECS の10周年を祝うリーダーシップセッションもございます。ご清聴ありがとうございました。ぜひフィードバックをお寄せください。Max と私からも、ご参加いただき誠にありがとうございました。
※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。
Discussion