re:Invent 2023: AWSが解説 .NETサーバーレスアプリの構築とLambdaの活用
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2023 - Build production-ready serverless .NET apps with AWS Lambda (XNT301)
この動画では、AWSのシニアクラウドアーキテクトJames EasthamとCraig Bossieが、.NETを使ったサーバーレスアプリケーションの構築について詳しく解説します。Lambda上でのASP.NETの実行、ネイティブAOTコンパイルによる劇的なパフォーマンス向上、Powertools for AWS Lambda .NETを活用した可観測性の実装など、.NET開発者にとって興味深いトピックが満載です。Infrastructure as Codeツールの選び方や、ドメイン駆動設計を用いたサーバーレスアプリケーションの構造化についても学べる、包括的なセッションとなっています。
※ 動画から自動生成した記事になります。誤字脱字や誤った内容が記載される可能性がありますので、正確な情報は動画本編をご覧ください。本編
サーバーレス.NETアプリケーション構築の導入
皆さん、こんにちは。本日お集まりいただいたのは、.NETを使ってサーバーレスアプリケーションを構築する最良の方法を学びたいと考える開発者やアーキテクトの皆さんです。それだけのことなんです。まず、皆さんの開発者としてのキャリアの中で、アプリケーションコードとは全く関係のない理由でアプリケーションが失敗した経験を思い出してください。ハードディスクの故障かもしれませんし、データセンター全体が過熱したのかもしれません。.NETでサーバーレスアプリケーションを構築すると、このような問題や課題の一部を回避し始めることができます。
これから55分間で、プロのサーバーレス.NET開発者になるために必要なスキルをすべて学んでいただきます。ビルドからテスト、デプロイ、運用まで、ソフトウェア開発ライフサイクル全体を通して学んでいきます。私はAWSのシニアクラウドアーキテクトのJames Easthamです。後ほど、同僚のCraig Bossieも加わります。それでは、さっそく始めましょう。まず手始めに、すでに何らかの形でLambdaを.NETで使用している方は挙手をお願いします。おお、たくさんの手が挙がりましたね。素晴らしいです。皆さんがサーバーレスの旅のどの段階にいらっしゃるかに関わらず、このセッションには皆さんにとって有益な内容が含まれています。
Lambdaの特徴と従来のアプリケーションとの違い
Lambdaを使い始める際に重要なことの1つは、Lambdaが従来のアプリケーション構築方法とは異なる点が多いということです。そこで、既存のアプリケーションの文脈でこれらの違いを見ていきましょう。この講演の残りの時間、皆さんは架空の銀行で働く開発者となります。これが皆さんのアプリケーションの全体的なアーキテクチャです。しかし、まずはこの小さな部分に焦点を当てていきます。皆さんはLambdaをバックエンドとし、DynamoDBにデータを保存するWeb APIを構築する必要があります。シンプルですね。
それでは、Lambdaが従来のモデルとどのように異なるのか、順を追って見ていきましょう。VMやコンテナ上でアプリケーションを構築する従来の方法では、こんな感じです。アプリケーションは一度起動し、リクエストがその環境に入ってきて、同じ実行中のアプリケーションインスタンス内で同時に処理されます。Lambdaでは、これが根本的に異なります。実行環境、つまりアプリケーションは、最初のリクエストが環境に到着するまで起動しません。そして、これらの環境はそれぞれ一度に1つのリクエストしか処理できません。
つまり、まったく同じミリ秒にLambdaに2つのリクエストが到着した場合、あるいは2つ目のリクエストが到着したときに既に1つのリクエストが処理中の場合、アプリケーション全体を再度起動する必要があります。その初期化フェーズを再度行う必要があるのです。これらの実行環境についてもう1つ興味深い点は、それぞれが専用のリソースセットを持つことです。関数に1ギガバイトのメモリを割り当てると、個々のリクエストは専用の1ギガバイトを得ることになります。これはサーバーベースのモデルとは異なり、サーバーベースでは全てのリクエストが同じリソースプール、同じメモリとCPUのプールを共有します。
Lambda が少し異なるもう一つの点は、リソース割り当ての仕組みです。既存のアプリケーションがあり、より多くのリクエストを処理する必要がある場合、例えば銀行に新規顧客が大幅に増えた場合、通常はより多くのリソースが必要になります。より多くのリソース、より大きなサーバー、より大きなコンテナは、通常、より多くのコストを意味します。Lambda を使い始めると、必ずしもそうではないことに気づきます。調整する必要があるリソースのノブは、メモリ構成だけだということがわかります。
CPU 集約型のワークロードを最適化している場合、「Lambda 関数の CPU をどうやって変更すればいいのか?」という疑問が生じるかもしれません。Lambda は、設定したメモリ量に比例して CPU を割り当てます。メモリ割り当てを増やすと、CPU とネットワーク帯域幅も同様に増加します。興味深いことに、これは、メモリ割り当て、パフォーマンス、コストが必ずしも線形的にスケールしないことを意味します。
Lambdaでの.NET開発:ASP.NETからLambda Annotationsまで
Lambda が異なる最後の点は、プログラミングモデル自体です。これは、単一目的のハンドラーと呼ばれる典型的な例です。このコードをすべて読む必要はありません。注目してほしいのはこの行です。なぜなら、このコードの中で、これが実際にビジネスロジックに関連する唯一の行だからです。残りのコードはすべてボイラープレートで、API Gateway から入ってくるインバウンドリクエストをドメインにマッピングし、そして再び外に出ていくレスポンスを構築しています。
今、「James、これは非常に異なるように見えますね。なぜこれをやりたいのでしょうか?なぜサーバーレスアプリケーションを構築したいのでしょうか?これらすべての異なることを考えなければならないのですか?」と思っているかもしれません。実際、最初に言いたいのは、必ずしもそうする必要はないということです。ビルドフェーズ、つまり Lambda 関数を実際に構築する方法を説明することで、それを示したいと思います。もちろん、皆さんは .NET 開発者で、多くの .NET コードを書く時間を費やしています。そして最初に言いたいのは、Lambda を使って構築する際にも、慣れ親しんだ、温かみのあるプログラミングモデルを維持できるということです。
これは、銀行で直面する状況です。既存の ASP.NET アプリケーションがあり、「これをそのまま Lambda で実行できたらすばらしいだろうな」と考えます。実際、まさにそれができるのです。Amazon.Lambda.AspNetCoreServer NuGet パッケージへの参照を追加すれば、既存の ASP.NET アプリケーションを Lambda 上で実行できます。
より伝統的な方法でASP.NETアプリケーションを構築している場合(つまり、minimal APIを使用していない場合)、プロジェクトに新しいクラスを1つ追加する必要があります。このクラスの名前は自由ですが、LambdaEntryPointのような適切な名前をお勧めします。このクラスで重要なのは、継承する基底クラスです。これは、Amazon Lambda ASP.NET Core Serverパッケージから提供される3つの異なる基底クラスのうちの1つになります。
これらの基底クラスにはすべてFunctionHandlerAsyncというメソッドがあり、リクエストが来たときにLambdaが実際に呼び出すのがこのメソッドです。このメソッドには、Lambdaに入ってくるリクエストをASP.NETが理解できるものに変換するロジックが含まれています。そして、出力時にも同様のことを行います。そのため、使用する基底クラスは、Lambdaの前に何を置くかによって異なります。なぜなら、Lambdaに入ってくるリクエストのペイロードは、API GatewayやApplication Load Balancerなど、何を使用するかによってわずかに異なるからです。
これらの基底クラスでは、Initメソッドをオーバーライドすることもできます。このInitメソッドで、ご覧のように、スタートアップコードを設定できます。ここでは、ASP.NETアプリケーションで既に使用しているのと同じStartupクラスを使用しています。もちろん、Lambda固有の初期化ロジックをここに追加することもできます。minimal APIを使用して新しい方法でAPIを構築している場合は、さらに簡単になります。Program.csファイルで、単にbuilder.Servicesを使ってAWS Lambdaホスティングを追加し、Lambdaの前に置くものを指定するenumを指定するだけで、このAPIはLambdaで実行する準備が整います。
これらのライブラリの本当に素晴らしい点の1つは、コンテキストを認識することです。つまり、この同じASP.NETアプリケーションをローカルマシンで実行し、コンテナで実行し、Lambdaで実行することができます。コンテキストを認識するので、Lambdaで実行されていることを理解し、それに基づいて異なる方法で起動し、実行します。これは非常に素晴らしいことです。なぜなら、素早く進めることができ、始めるのが容易で、Lambdaを使用することのいくつかの利点を得ることができるからです。また、慣れ親しんだプログラミングモデルを維持できます。単にASP.NETを使って構築しているだけです。とてもシンプルです。
Lambda関数のパフォーマンス最適化とベンチマーク
ただし、トレードオフもあります。これらのトレードオフは特に、関数が起動する際の初期化に関連しています。ここで見ているのは、単一目的で構築された.NET 6関数と、Lambdaで実行されるminimal APIの比較です。これは全く同じアプリケーションです。そこに見えるQRコードは、.NETをLambdaで実行するさまざまな方法のベンチマークがあるGitHubリポジトリに誘導します。実行環境が利用可能になり、ウォームになると、パフォーマンスはかなり comparable であることがわかります。.NETはパフォーマンスの高い言語です。皆さんもそれはご存知でしょう。
しかし、スタートアップフェーズを見始めると(Lambdaではこれがよく起こることを覚えておいてください)、スタートアップに大きな違いがあることがわかります。これについて非常に興味深いのは、これらのベンチマークを実行する際、10分間で1秒あたり100リクエストを実行し、それは通常約15万リクエストに相当するということです。Lambdaで最小限のAPIを実行する場合、通常これは約90回のコールドスタートになります。つまり、コールドスタートは長くなりますが、その頻度は少なくなる可能性が高いのです。単一目的のハンドラーの場合、通常300〜400回のコールドスタートになります。
しかし実際、あなたのバンキングアプリケーションでは、これは必要なものとは少し異なります。もう少しパフォーマンスの高いものが必要です。この馴染みのあるプログラミングモデルを維持したいけれど、ASP.NETは使いたくない。そんな時、どうすればいいでしょうか?ここで、Lambda annotations frameworkを紹介したいと思います。Lambda annotations frameworkを使ったことがある人はいますか?実際、聞いたことがある、または使ったことがある人は?数人の手が挙がりましたね。素晴らしい。
では、先ほどの例、ビジネスロジックを実行するコードが1行だけの単一目的ハンドラーの例を再び見てみましょう。ここで指を鳴らすと、これは Lambda annotations を使って書かれた全く同じアプリケーションです。ご覧の通り、これはシンプルです。純粋にビジネスロジックに焦点を当てています。メソッドがあり、そのメソッドに2つの属性が追加されているだけです。最初の LambdaFunction は、このメソッドが独立した Lambda 関数になることを Lambda annotations に伝えます。これにより、ASP.NET でコントローラークラスを作成する場合と同様に、同じコードファイル内に複数の関数を定義できます。
そして2つ目の属性である HttpApi は、この Lambda 関数の前に API を配置することを示しています。これは .NET のソースジェネレーターを使用して動作します。コンパイル時に、Lambda annotations はこれらの属性を探し、実際にすべてのボイラープレートコードをコンパイル時に生成します。つまり、そのコードは存在しますが、もはや気にする必要がないのです。私たちがそれを生成します。
これにより、Lambda で依存性注入を使用することもできます。お馴染みの Startup.cs ファイルは健在です。それに LambdaStartup で注釈を付けることができ、コードをコンパイルすると、同じことが起こります。ソースジェネレーターが、Lambda で依存性注入を使用できるようにするためのすべての配線を生成します。ただし、ここで注意が必要です。依存性注入にアクセスできるからといって、アプリやスタートアップに大量の依存関係を追加すべきではありません。スタートアップフェーズで行う作業は、すべてコールドスタート時間に直接影響を与えるからです。
これが通常、ASP.NETの方が若干コールドスタートが長くなる理由です。ASP.NETを起動する必要があるからですね。しかし、Lambdaの素晴らしい点の1つは、非常に具体的に最適化できることです。つまり、このバンキングアプリケーションから始めて、API Gatewayの背後でLambda上でASP.NETを実行し、 すべてのエンドポイントを同じ関数にルーティングすると、最初はうまくいきます。
そして実際には、APIの10個のエンドポイントのうち9個について、 ASP.NETの起動で問題ないことに気づきます。そこで面白いのは、必要な箇所だけを最適化し始められることです。API Gatewayレベルのルーティングを使用して、エンドポイントの1つを分離することができます。 例えば、アカウント詳細を取得するエンドポイントを別々に実行するかもしれません。そして、それを異なる方法で最適化できます。そこで自問自答することができます。 何を最適化したいのか?レイテンシーなのか?純粋なパフォーマンスなのか?リソース要件やセキュリティ要件が異なるのか?
そして、それを別の関数に分割し、大半のエンドポイントではASP.NETを実行することができます。Lambda上でASP.NETを実行したり、複数のエンドポイントを同じLambda関数で実行したり、いわゆるLambda-lithを使用したりするのは全く問題ありません。なぜなら、先ほど言ったように、コールドスタートは長くなるかもしれませんが、通常はその頻度は少なくなるからです。.NETを使用した低レイテンシーのイベント駆動アプリケーションの構築についてもっと学びたい方は、今週後半に行われるセッションSVS308をぜひチェックしてください。AWSのMarcia VillalbaとTim Bruce、そしてSecond DinnerのBrenna Mooreが登壇します。Second Dinnerは、完全にサーバーレスと.NETを使用して、1位にランクされたモバイルゲームを構築しました。つまり、パフォーマンスを出すことができ、本番環境で使用できるのです。
サーバーレスアプリケーションの可観測性とPowertools for AWS Lambda .NET
これで開発者として素晴らしい立場に立てます。ASP.NETを使い慣れた場所で使用し、特定のエンドポイントを分離して必要な箇所だけを最適化することができるのです。そして、これを数週間本番環境で実行した後、ほとんどの部分に満足していますが、レイテンシーに関していくつかの問題や課題がまだ見られます。ここで、.NET Lambda関数の可観測性と計装についてもっと詳しく話すために、Craigにバトンタッチしたいと思います。
さて、サーバーレスでアプリケーションを実行することで得られる信頼性、スケーラビリティ、そして回復力の恩恵を受けることができました。 しかし、アプリケーションにエラーやパフォーマンスの問題がある場合、それらをどのように追跡すればよいでしょうか?これらの課題を簡単に克服する方法があり、ここに示すような厄介な状況を回避するのに役立つ標準的なツールとプロセスがあると言ったらどうでしょうか?私はCraig Bossieと申します。AWSのSolutions Architectで、.NETとサーバーレスの愛好家です。可観測性について話しましょう。
先ほど話していた銀行アプリケーションに戻りましょう。ユーザーがシステムと対話する際、APIを呼び出してローンに関する情報を送信し、それがLambda関数を呼び出します。そのLambda関数は、別のLambda関数を使って下流のAPIを呼び出し、DynamoDBテーブルを更新するかもしれません。そして処理が完了すると、EventBridgeバスにメッセージを送信し、それがステップ関数をトリガーします。このステップ関数は、借り手が記入するフォームを作成するために、多数のLambda関数を実行します。すべてが完了すると、メッセージがキューに入れられ、最終的にはユーザーにフォームを記入してもらうためのメールとして送信されます。
この簡略化されたプロセスでも、12個のコンポーネントがあります。実際のシステムでは、おそらくもっと多くのコンポーネントがあるでしょう。サーバーレスアプリケーションのこの分散的な性質のため、システム内で何が起こっているかを把握することが特に重要になります。そして、オブザーバビリティとは、アプリケーションの内部状態を理解する能力のことです。オンプレミスでモノリシックなアプリケーションを実行している場合、すべてが1つか2つの大きなコードベースに集中しているため、問題を追跡するのは簡単かもしれません。サーバーレスの場合は確実に異なりますが、必ずしも難しくなるわけではありません。
オブザーバビリティについて話すとき、3つの柱について言及します。これらはトレーシング、ロギング、メトリクスです。これらは、サーバーレスアプリケーションのパフォーマンスを理解する上で非常に重要です。ここで詳細に入る前に、あらかじめお伝えしておきたいのは、Lambda関数でオブザーバビリティを実装するのが非常に簡単だということです。Powertools for AWS Lambda .NETは、NuGetで配布されているライブラリのセットで、Lambda関数の運用を支援するように設計されています。これらは、コードベースをボイラープレートで埋め尽くしたり、独自のオブザーバビリティソリューションを作成したりすることなく、関数内でオブザーバビリティのベストプラクティスを実装し、観察するのに役立つシンプルなパターンを提供します。また、.NET用のPowertoolsは、オブザーバビリティを超えて、べき等性やパラメータなどの機能も提供します。
サーバーレスアプリケーションを開発している場合、Powertoolsの設定は非常に簡単です。NuGetパッケージをインストールした後、コードで直接設定するか、より一般的には環境変数を使用します。ここでは、ロギング、トレーシング、メトリクスのパラメータが表示されており、これらは受信イベントの収集、メトリクスのサンプルレート、メトリクス名前空間などを制御します。これは特にサーバーレスの文脈で有用です。なぜなら、インフラストラクチャ・アズ・コードを使用する場合、サーバーレスアプリケーション内のすべてのLambda関数に対して、オブザーバビリティの設定を統一的に適用できるからです。
では、Powertoolsがどのようにしてコードと生活の両方をシンプルにするかを見ていきましょう。システム全体を通じて単一のユーザーリクエストを追跡できたら便利ではないでしょうか?これらの独立したコンポーネントが何らかの形で相関付けられ、互いを認識できるようになるかもしれません。これがトレーシングの役割です。AWSでは、X-Rayが分散トレーシングに使用されるサービスです。ユーザーがリクエストを行うと、トレースIDが生成されます。例えば、API Gatewayを呼び出す際にです。そのリクエストIDは、その後下流のサービスに渡され、さらにそのサービスが下流のサービスにIDを渡します。この過程で、すべてのサービスがX-Rayサービスにデータを報告するため、アプリケーション内のすべてのアクティビティを相互に関連付けることができます。
X-Ray サービスでは、ここに示すレスポンスタイムのような、各コンポーネントの個別のデータを収集できます。しかし、アプリケーション全体のデータを報告するためには、IDが各サービスに正しく引き継がれていることを確認する必要があります。そして、すべてのサービスがそれを同じように扱うわけではありません。これは単に対処すべき複雑さが増すだけだと思われるかもしれません。 しかし、嬉しいことに、コードを書く際には、.NET Lambda 関数でその連続性を維持し、トレース ID が正しく引き継がれるようにすることは実際にはとても簡単です。また、他のサービスやダウンストリームのコンポーネントへの呼び出しを計測して、それらのパフォーマンスを理解することができます。
トレーシング、ロギング、メトリクスの実装
AWS SDK for .NET を使用している場合、単一のメソッド呼び出しだけで、Lambda コード内からダウンストリームの AWS サービスへのすべての呼び出しを計測できるようになります。 外部の HTTP サービスを使用している場合は、それらを計測するためのライブラリがあり、システムのパフォーマンスにどのような影響を与えるかを理解することができます。また、データベースがアプリケーションのパフォーマンスのボトルネックになることがよくあることは、皆さんもよくご存知だと思います。 そこで、SQL Server の呼び出しを計測するためのライブラリがあり、これらの問題をより詳しく調査することができます。
しかし、時には関数内のパフォーマンスを理解したいこともあり、Powertools はそれを本当に助けてくれます。 例えば、コードの特定の部分に関するデータを収集し、コールドスタートがいつ発生しているかを理解したいかもしれません。そのためには、少しハックが必要です。また、例えばコードの異なるセグメントにパフォーマンスの問題がある場合、 それらに関するデータを収集することができます。トレースでは、これらをサブセグメントと呼びます。X-Ray では、サブセグメントの開始時に X-Ray を呼び出し、終了時にサブセグメントを終了する別の呼び出しを行う必要があります。しかし、これが多くなると複雑になる可能性があります。
しかし、すべてが完了すると、素晴らしいトレースが得られ、アプリケーションの個々の部分に関する情報が得られ、サーバーレスアプリケーションのサービスマップが得られます。しかし、Powertools はこれをはるかに簡単にしてくれます。ハンドラーに tracing 属性を追加するだけで、先ほど話したコールドスタートの問題が自動的に処理されます。設定するだけでよいのです。また、以前はサブセグメントを収集するために、各セグメントの開始と終了を行う必要がありました。しかし、Powertools を使えば、Lambda 関数内の個々のメソッド呼び出しに tracing 属性を追加し、 サブセグメント名を指定するだけです。そうすれば、そのメソッドが呼び出されるたびに、Powertools が X-Ray にサブセグメントとして報告します。したがって、以前と同じ情報をより少ないコードで収集できるのです。
さて、.NET Lambda 関数を書く際、デフォルトで CloudWatch へのログ記録が有効になっています。最終的に、Lambda は標準出力や標準エラーに送られたものをすべて拾い上げ、CloudWatch のロググループに送信します。
Lambdaを使ったことがある方なら、おそらくすでにご存知でしょう。Lambdaには、ログを記録するための組み込みの方法がいくつかあります。console.logを本質的にカプセル化したLambda logger.logメソッドを使用するか、すべてのLambdaハンドラに渡されるcontextオブジェクトのloggerプロパティを使用できます。これらのメソッドでは、ログの重要度レベルを指定することができます。
最近リリースされたLambdaの高度なログ制御機能により、非構造化またはOpenTelemetry準拠の構造化ログを、Lambda関数内で自動的に収集できるようになりました。しかし、ハンドラに関するより詳細な文脈情報を自動的に収集したい場合はどうすればよいでしょうか?Powertoolsのロギング機能を使用すると、関数にlogging属性を追加するだけで済みます。この単一の設定により、Lambdaファンクションをトリガーしたイベントをログに記録することができます。
Powertoolsに付属のloggerクラスを使用することで、すべてのログエントリに追加のコンテキストを含む統一されたログ構造を作成できます。このコンテキストには、X-RayトレースID、コールドスタートかどうか、および関数に関するその他の詳細情報などが含まれます。オブジェクトを渡すこともでき、自動的に構造化ログにシリアライズされます。この構造化された形式は、CloudWatch log insightsのようなツールを使用してログをクエリする際に特に便利です。
CloudWatchは、AWSのほぼすべてのコンピューティングサービスのメトリクスを収集するのに優れており、Lambdaも例外ではありません。Lambdaは、実行時間や同時実行数など、その動作に特化した複数の標準メトリクスを提供しており、アプリケーションのパフォーマンスを理解するのに役立ちます。しかし、アプリケーションの動作をより深く理解するために、パフォーマンスやビジネスロジックに関連するカスタムメトリクスを収集したい場合もあるでしょう。
AWS SDK for .NETを使用する場合、カスタムメトリクスを収集できます。ここに、アップロードされたファイルのサイズなど、1つのデータポイントを収集できる約20行のコードの例があります。多くのコードに見えるかもしれませんが、一度実装すれば、このメトリクスをダッシュボードやCloudWatchアラームで使用できます。しかし、Powertoolsらしく、このプロセスを簡略化することができます。ハンドラにmetrics属性を付けるだけで、多くの設定が自動的に追加されます。その後、metrics.add_metricメソッドを使用して、同じデータをはるかに少ない行数でログに記録できます。
では、これらすべてを組み合わせると何が得られるのでしょうか?Lambda関数のトラブルシューティングを3つの別々の方法で行うだけなのでしょうか?そうではありません。相関のあるトレース、ログ、メトリクスを組み合わせることで、アプリケーションの包括的な可視性が得られます。アプリケーションの大部分のマップを表示し、個々のリクエストを1人のユーザーのアクティビティまで追跡できます。自動収集されたメトリクスとカスタムメトリクスの両方を使用して、アプリケーションの個々のコンポーネントのパフォーマンスを理解できます。
相関のあるログを使用すると、Lambda関数の深部で何が起こっているかをより良く理解できます。これらすべてを組み合わせると、Powertoolsを使用しない場合、観測可能性の要件が複雑になればなるほど、より多くのコード行を書く必要があり、それは複雑になる可能性があることがわかります。私たちが議論した技術を使用することで、すべての機能を維持しながら、コードベースのボイラープレートコードを大幅に削減できます。
Lambda関数のコールドスタート最適化
このデータを収集する方法がわかったところで、どのようにしてパフォーマンスを向上させるのでしょうか?ここからはJamesが説明します。Jamesは次のように始めます。「私たち開発者は興味深い段階にいます。Lambda関数のスイートを構築し、ベストプラクティスを使用して構築し、それらを理解するための観測可能性と計測を手に入れました。今度は、それを高速化する方法を知りたいと思います。Lambdaで望むパフォーマンスをどのように得られるでしょうか?この次のセクションでは、Node.jsなどの言語を使用する場合と同じパフォーマンスをLambdaで.NETを使用して達成する方法をお見せします。そのために、先ほど議論した同じアーキテクチャを再度見てみましょう。」
LambdaでASP.NETを実行し、さらに別途最適化するために分離した単一の関数があります。それがここにあるものです。そして今や、観測可能性と計測を手に入れたことで、その特定の関数を深く掘り下げ、問題の原因を正確に見ることができます。そして実際に、問題の原因がコールドスタートであることがわかります。.NETとLambdaについて話すときの象徴的な問題ですね。
では、コールドスタートを最適化する方法、これらの問題をどのように軽減できるかを探ってみましょう。具体的な方法に入る前に、いくつか指摘しておきたいことがあります。まず、定常状態の負荷下では、コールドスタートは通常、呼び出しの1パーセント未満を占めるに過ぎません。先ほど見たベンチマークでは、通常、リクエストの約0.4%がコールドスタートです。次に考慮すべき点は、開発者として、関数の新しいバージョンを公開するたびに、必ずコールドスタートが発生するということです。そのため、Lambdaの使用を開始し、コールドスタートがワークロードの問題になるかどうかを判断する際には、関数をデプロイして1回実行し、「ああ、コールドスタート、これは私には向かないな」と考えるだけではいけません。実際の世界で実行されるような、実際の負荷がかかった状態で関数を実行してみてください。そうすることで、コールドスタートが実際に問題を引き起こすのか、それとも問題にならないのかがわかります。
最後のポイントは、コールドスタートは通常、同期呼び出しにのみ関連するということです。非同期の作業やキューからメッセージを読み取る場合、ほとんどのユースケースでは、800〜900ミリ秒のコールドスタートが大きな問題を引き起こすことはありません。もちろん、場合によっては問題になることもあります。では、コールドスタートを最適化する方法を見てみましょう。実際に何が起こっているのかを詳しく見ていきます。Lambdaにリクエストが来ると、実行環境を作成する必要があります。環境が作成された後、関数コードがダウンロードされ、.NETランタイムが起動し、その後コードの初期化が行われます。オブジェクトがインスタンス化され、コンストラクタ内のコードが実行されます。この全期間がコールドスタートです。これが実際に起こっていることです。
すべてが完了すると、実際のペイロードが関数に渡され、実際のリクエストが呼び出され、ハンドラが実行されます。これを考えると、開発者として制御できる部分は実際にはこの2つだけです。バンドルのサイズ(コードの量)と、実際の初期化フェーズです。そこで、すぐに始められる2つのことは、バンドルサイズをできるだけ小さく保つこと、依存関係を最小限に抑えること、そしてコードの起動時に行う作業を最小限に抑えることです。
もちろん、これら2つのことを行っても、コールドスタートに課題が残る場合があります。そこで、他に検討できることがあります。Lambdaのプロビジョンドコンカレンシーという機能を利用することができます。これは、コードを1行も変更せずに有効にできる機能です。これはLambdaの機能で、初期化フェーズを事前に実行します。そのため、一連の実行環境が常に稼働している状態になり、必要な数を設定できます。例えば、10個のプロビジョニング環境が欲しいと指定できます。これは非常に素晴らしいパフォーマンス機能です。コールドスタートを軽減するために特別に開発されました。
さて、皆さんがまず考えるのは、コストはどうなるのか?ということでしょう。プロビジョンドコンカレンシーにはお金がかかりますよね?常に実行環境が稼働しているということは、コストがかかるはずです。実際の答えは、単にお金がかかるというよりも、もっと微妙なものです。では、Lambdaの価格設定とその仕組みを簡単に見てみましょう。これはUS East 1の価格設定で、地域によって若干異なります。オンデマンド価格設定はシンプルです。関数の実行時間をミリ秒単位で正確に支払うだけです。プロビジョンドコンカレンシーのコストは2つの数字に分かれています。プロビジョンドコンカレンシーのコストは、プロビジョニングされた環境が利用可能である限り常に支払うものです。そして、呼び出し期間のコストがあり、これはオンデマンドと同じでミリ秒単位で課金されます。
数学が得意な方や、非常に観察力の鋭い方なら、プロビジョンドコンカレンシーの2つの数字を足すと、実際にオンデマンドよりも安くなることに気づくでしょう。つまり、プロビジョンドコンカレンシーを使用し、用意した環境を最大限に活用すれば、オンデマンドを使用するよりも約16%安くなるということです。US East 1での分岐点は約60%です。プロビジョニングした環境の60%以上を使用していれば、通常はコストが低くなります。これはLambdaの非常に興味深い点の1つです。なぜなら、コストを下げながら同時にパフォーマンスを向上させることができるからです。
これを実現するには、当然ながら比較的安定したワークロードが必要です。Application Auto Scalingを使用してプロビジョニングされた環境を自動的にスケールアップおよびダウンできますが、もちろん、適切な数の同時実行環境を実際に得るためには、ある程度の定常状態のワークロードが必要です。すべてのワークロードに適しているわけではありませんが、定常状態がある場合、これによってパフォーマンスが向上し、コストも削減できます。
では、これがうまくいかない場合、他にどのような選択肢がありますか? 最初の、そして実際に非常に重要な選択肢は、Lambda関数を適切なサイズにすることです。関数に適切なリソース割り当てを行うことで、コールドスタートを最小限に抑え、実行時間とコストを最小限に抑えることができます。適切なサイズにするとは、関数に128メガバイトから10ギガバイトのメモリを割り当てることができるということです。そして、すべての関数には最適な割り当てがあります。
ネイティブAhead-of-Time (AOT) コンパイルによるパフォーマンス向上
一つの方法は、関数を手動で調整し、メモリ設定を変更してテストすることですが、これにはかなりの時間がかかるでしょう。もう一つの選択肢は、Lambda Power Tuningを使用してこのプロセス全体を自動化することです。Lambda Power Tuningは、アカウントにデプロイできるもので、内部的にStep Functionsを使用しています。Step Functionを呼び出し、テストしたいLambda関数、サンプルペイロード、テストしたいメモリ設定の配列を渡します。Power Tuningは、これらのさまざまな割り当てで関数を何度も実行し、このようなグラフを提供します。
青い線はコストを、ピンクの線はパフォーマンス、つまり呼び出し時間を示しています。予想通り、128メガバイトから10ギガバイトのメモリの間では、コストにかなりの差があります。10ギガバイトの方が128メガバイトよりもコストがかかります。しかし、興味深いのは、128メガバイトから2ギガバイトの間では、コストにほとんど変化がないことです。ほぼ同じです。しかし、同じ期間で呼び出し時間は大幅に短縮されます。この特定の関数では、2ギガバイトがちょうど良いポイントです。
ここでのポイントは、私がLambdaを使い始めたときのように、「128メガバイトはミリ秒あたりが最も安いから安いだろう」と考えて、ただそれを使うのではないということです。実際には、特定のワークロードを見て、Power Tuningのようなツールを使用して最適なポイントを見つけることが重要です。通常、.NETの場合、1ギガバイトあたりが始めるのに適しています。そこから始めて最適化していけば、通常最高のパフォーマンスが得られます。これを行って適切なメモリ割り当てを行っても、パフォーマンスが十分に速くない場合があります。関数を、先週ここラスベガスを疾走していたドライバーのように速くしたいかもしれません。とにかく速くしたいのです。
ここで、ネイティブ ahead-of-time コンパイルが役立ちます。ネイティブ AOT について聞いたことがある人や使ったことがある人はいますか?ネイティブ AOT は .NET の新機能で、.NET 7 で GA となり、.NET 8 でさらに改良されました。これにより、.NET アプリケーションのネイティブバイナリを生成でき、JIT の必要性がなくなり、アプリケーションの起動時間が劇的に改善されます。劇的と言いましたが、先ほどのベンチマークを再度見てみましょう。ここでは、.NET 6 の単一目的ハンドラと、.NET 8 でネイティブコンパイルされた全く同じ関数を比較しています。ウォームスタートの数値は同等で、ほぼ同じです。.NET が高速なのはご存知の通りです。
コールドスタートについては、数値が劇的です。 P50(50パーセンタイル)で62%、P99で59%のパフォーマンス向上が見られます。.NET と Lambda を使用して、一貫して400-500ミリ秒未満のコールドスタートが実現しています。これは、同じアプリケーションアーキテクチャを使用した Go ランタイムと同等です。.NET 8 で、Microsoft はさらに一歩進めました。Microsoft は ASP.NET とネイティブ AOT の限定サポートを発表しました。もう一度同じ数値を見てみましょう。 .NET 6 で Lambda 上で最小限の API を実行した場合と、.NET 8 でネイティブコンパイルされた全く同じ最小限の API を比較しています。ウォームスタートの数値は同じです。コールドスタートでは、再び劇的なパフォーマンスの向上が見られます。P50 で起動時間が71%、P99 で53%改善されています。
つまり、ネイティブ AOT を使用した場合のコールドスタートの数値は劇的で、改善は素晴らしいものです。サーバーレスの .NET アプリケーションを構築する上で、絶対的なゲームチェンジャーと言えるでしょう。もちろん、.NET とネイティブ AOT に詳しい方なら、すでにご存知でしょう。そうでない方のために、ネイティブ AOT を使用する際のトレードオフについて説明しましょう。実際に Lambda でネイティブ AOT を有効にする方法と、対処すべきトレードオフについて見ていきましょう。
Lambda関数でのネイティブAOTの実装方法
まず最初に、生成するアセンブリ(実行可能ファイル)の名前を bootstrap にする必要があります。通常、Lambda でネイティブ AOT を使用する場合、カスタムランタイムを使用する必要があります。Lambda のカスタムランタイムの仕組みは、bootstrap という名前のファイルを探し、そのファイルを実行するだけです。したがって、生成するバイナリの名前を bootstrap にする必要があります。次に、publish AOT フラグを true に設定する必要があります。これにより、.NET コンパイラ にこのアプリケーションをネイティブコンパイルするよう指示します。
ネイティブ AOT のトレードオフの1つは、アプリケーションを実行時と同じオペレーティングシステムとプロセッサアーキテクチャ上でコンパイルする必要があることです。では、開発マシンとして Amazon Linux 2 や Amazon Linux 2023 を使用している人はどれくらいいますか?誰もいませんね。予想通りです。では、これは何を意味するのでしょうか?Lambda が内部で使用している Amazon Linux 2 上で実行できるように Lambda 関数をコンパイルするには、実際にそれを使用していないのに、どうすればよいのでしょうか?
実は、この機能を .NET Lambda ツールに組み込んでいます。Lambda CLI for .NET を使用して、これらの2つのコマンドのいずれかで関数をパッケージ化またはデプロイする場合、この機能が組み込まれています。CLI ツールは published AOT フラグを探し、検出すると Amazon Linux 2 の Docker イメージをダウンロードします。そして、その実行中の Docker コンテナ内でアプリケーションをコンパイルし、 実行可能ファイルをローカルファイルシステムに出力します。もちろん、その実行可能ファイルを実行しようとすると、Amazon Linux 2 を使用していないため失敗します。これが私たちが克服した1つの課題です。これで正しい OS 上でコードをコンパイルできるようになりました。
次の課題は、カスタムランタイムを使用する場合、.NET ランタイムが存在しないことです。それは単なる空の Lambda 環境なので、.NET ランタイムを自分でブートストラップする必要があります。そのため、Lambda で native AOT を使用する場合は、アプリケーションに static main メソッドを追加する必要があります。 これが実行可能ファイルが使用するエントリーポイントになります。そして、その static main メソッド内で、Lambda ランタイムをブートストラップすることを確認する必要があります。ご覧のように、LambdaBootstrapBuilder.Create メソッドを使用しています。使用するハンドラーを渡し、それから使用したいシリアライザーを渡しています。
もちろん、これはより多くのボイラープレートコードで、native AOT を使用する際に追加する必要があります。native AOT を使用しない場合は、これを削除する必要があります。考えることが多いですよね?ただし、Lambda Annotations Framework を使用している場合は違います。Lambda Annotations は約3週間前から、 新しい LambdaGlobalProperties 属性を持っています。この属性で GenerateMain プロパティを true に設定できます。これにより、コンパイル時にソースジェネレーターを使用して、先ほど見た static main メソッドを自動生成します。つまり、そのボイラープレートコードを追加する心配をする必要がないのです。ご覧のコードは、Lambda 上でネイティブコンパイルされて実行する準備が完全に整っています。
これの素晴らしい点の1つは、アプリケーションコードを1行も変更せずに、.NET の異なるバージョン間や、native AOT とそうでないものの間を移動できることです。このフラグを true から false に切り替えたり、プロジェクトファイルの PublishAOT を変更したりするだけで、実際のアプリケーションコードをほとんど変更する必要がありません。内部的には、これが Lambda Annotations によって実際に生成されるものです。これをすべて見る必要はありません。 重要なのは、ここの上部にあるスイッチ文です。生成される static main メソッドは、スイッチ文を使用してどのハンドラーが使用されるかを決定します。これにより、単一のバイナリを生成し、同じバイナリ内に複数の Lambda 関数を定義して、それを複数の異なる Lambda 関数にデプロイできます。そして、実際に使用されるハンドラーを決定するために、annotations handler 環境変数を設定するだけで済みます。来年、これをさらに改善して、より簡単で良いものにする予定です。
native AOT で考慮する最後のことは、JSON のシリアル化と逆シリアル化です。native AOT を使用すると、.NET のリフレクションベースの機能の多くと、トリミングの仕組みにより、 System.Text.Json が実際には機能しなくなります。単に JsonSerializer.Serialize を使用することはできません。そのため、ソース生成されたシリアライザーを使用する必要があります。これは .NET に追加された機能で、.NET 5 で導入されたと思います。これにより、コンパイル時に JSON 操作に必要なコードを生成できます。プロジェクトに新しい partial クラスを追加します。先ほどと同様に、好きな名前を付けることができます。また banana でも構いません。ただし、CustomSerializationContext のような適切な名前を使用することをお勧めします。
重要なのは、継承元のベースクラスです。JsonSerializerContextクラスを継承します。これはSystem.Text.Json名前空間から来ています。そして、このクラスにいくつかの属性を追加します。これらの属性は、JSONシリアライズおよびデシリアライズに利用可能にしたいオブジェクトを決定します。ここでは、私のアプリケーションコードに固有のLoanとLoanWrapperという2つのクラスがあります。そして、Lambdaに固有の2つのオブジェクト、実際のAPI Gatewayイベントペイロードがあります。JSONシリアライズまたはデシリアライズが必要なすべてのクラスに対してこれを行う必要があります。Microsoftのサイトには、ソース生成シリアライゼーションの方法について多くの優れたドキュメントがあります。
Lambdaにとってもう一つ重要なのは、上部にあるLambdaSerializer属性の行です。.NETランタイムにソース生成シリアライザーを使用するよう指示する必要があります。Lambda serializerをSourceGeneratedLambdaJsonSerializerに設定し、カスタムコンテキストを型引数として渡す必要があります。これで終わりです。LambdaでネイティブAOTを有効にするのはこれだけです。アセンブリ名を更新し、PublishAOTフラグを設定し、静的なmainメソッドを生成していることを確認し、JSONシリアライゼーションとデシリアライゼーションを処理していることを確認すれば、ネイティブAOTの利点を使い始めることができます。
サーバーレスアプリケーションのテスト戦略
もちろん、先ほどのアーキテクチャ例に戻ると、LambdaでのASP.NETは多くのユースケースで適切なパフォーマンスを提供する可能性があります。そのため、これらのトレードオフを追加する必要があるため、絶対に必要な場合にのみネイティブAOTを追加することをお勧めします。ネイティブAOTでは、実行時にのみ発生する多くの問題があります。例えば、トリミングの仕組みにより、コード内のどこにも使用されていないクラスのプロパティがある場合、トリマーがそれをトリミングしてしまう可能性があります。そのため、本当に必要な場所でのみ使用し、使用する場合はアプリケーションをテストしてください。そして、サーバーレスアプリケーションのテストは少し異なります。通常、サーバーレスアプリケーションは、Craigが先ほど示したように、多くのネイティブサービス統合を使用しているからです。SQS、SNS、EventBridgeなどがあるかもしれませんが、これらのサービスをすべてローカルでエミュレートし、典型的なASP.NET、SQL Serverタイプのアプリケーションで行うように、アプリケーション全体をローカルでエンドツーエンドテストしようとするのは望ましくありません。
そのため、サーバーレスの一般的な考え方は、ローカルでユニットテストを行い、それ以外のすべてを実際のクラウドリソースに対してテストすることです。これは、コアのビジネス機能をテストするための少数のユニットテストを書き、より堅牢なテストを行う際には、実際のクラウドリソースにプッシュアウトすることを意味します。
テストについては長く話しませんが、今週後半のXNT 308をお勧めします。そこでは、AWSのシニアMicrosoftスペシャリストソリューションアーキテクトであるDror Helperが、.NETクラウドネイティブアプリケーションのテストに関する全セッションを行います。ただし、.NETとLambdaを使用し、Annotationsフレームワークを使用している場合は、テスト可能な方法で関数を書いていることを確認してください。Annotationsを使用すると、依存性注入を使用しているため、関数に関連する要素をモック化できます。例えば、IAccountRepositoryのモック実装を注入し、それをユニットテストに使用し、その後クラウドにプッシュアウトして、実際のクラウドリソースに対して実際の統合テストを実行することができます。
さて、これらの作業を終えたことで、開発者としてさらに有利な立場に立つことができました。なぜなら、明確に定義され、適切に構築された Lambda 関数を手に入れたからです。 観測可能性と計装を備え、単一目的のハンドラーによるパフォーマンス向上や、Lambda 上での ASP.NET による優れたパフォーマンスを実現する能力を手に入れました。そして今、あとはデプロイするだけです。ここで、.NET Lambda 関数のデプロイについて Craig に話を引き継ぎたいと思います。
Infrastructure as Codeによるサーバーレスアプリケーションの管理
ありがとう、James。さて、ご想像の通り、素晴らしい Lambda コードを持っていても、それをクラウドに効率的に展開する方法がなければ、あまり意味がありません。残りの時間で、本番環境に対応した .NET サーバーレス Lambda アプリケーションをクラウドに展開し、可能な限り効率的に実行する方法を探ってみましょう。クラウドでアプリケーションを実行し始めると、 どのサーバーで実行するか、どのフォルダに DLL を配置するかといった考え方を超える必要があります。クラウドベースのサーバーレスアプリケーションは、高度に分散されたイベントベースのアプリケーションです。同じプロセスで動作するわけでもなく、同じハードウェアで動作するわけでもなく、多くの場合、同じデータセンターにさえありません。しかし、それらはすべて抽象化されています。
.NET ベースのサーバーレスアプリケーションには、もちろん多くの Lambda 関数があり、 アプリケーションの複雑さによっては数十以上になる可能性もあります。 しかし、他のサービスも存在します。例えば、S3 や DynamoDB を使用するストレージ、API Gateway や SQS のような通信サービス、Cognito のようなセキュリティサービスなどのサポートサービスがあります。多くの要素がありますね。これらをどのように管理すればよいのでしょうか?答えは、Infrastructure as Code です。
Infrastructure as Code がどのように役立つかをよりよく理解していただくために、 銀行アプリの文脈で、典型的なサーバーレス .NET アプリケーションの一部を見てみましょう。この例では、顧客がドキュメントをアップロードすると、最終的に S3 バケットに保存されます。それが Lambda 関数をトリガーし、そのドキュメントに対して処理を行います。そして、処理が完了すると、SNS トピックにパブリッシュして、下流の購読者に完了を通知します。
サーバーレスの人気が高まったため、実際にこれらのアプリケーションの構築を支援するための Infrastructure as Code フレームワークがいくつか登場しました。その1つが AWS SAM、つまり Serverless Application Model です。これは宣言的なテンプレート構文を使用し、 CloudFormation のスーパーセットです。ここに示しているのは、先ほど説明したインフラストラクチャを定義する実際の SAM テンプレートの一部です。ここでバケットを定義し、ここで SNS トピックを定義し、Lambda 関数を定義する場所もあります。
しかし、SAMの強力な点の1つは、コンポーネント間の相互作用を簡素化できることです。バケットがLambda関数をトリガーし、Lambda関数がSNSにパブリッシュするという要望を表現するだけで、SAMが後は全て処理し、バックグラウンドで必要なコードを生成してくれます。また、Lambda関数に適切な権限を適用し、タスクを実行するために必要最小限のセキュリティ権限のみを持つようにします。
さて、Cloud Development Kit、つまりAWS CDKは、 AWSでサーバーレスアプリケーションを構築するために頻繁に使用される、もう1つのインフラストラクチャ・アズ・コードのフレームワークです。CDKの際立つ特徴の1つは、お気に入りのプログラミング言語を使用して、実際のコードとしてインフラストラクチャを構築できることです。おそらく、この会場にいる皆さんのほとんどにとって、それはC#でしょう。 これは、私たちが話してきたインフラストラクチャを定義するC#コードの一部です。S3バケットをインスタンス化し、SNSトピックを作成し、データを処理する関数を定義しているのがわかります。SAMと同様に、コンポーネント間の相互作用を表現するだけで、 CDKが配線を行ってくれます。最終的に、CDKはバックグラウンドでCloudFormationを生成しますが、それを気にする必要はありません。お気に入りのプログラミング言語であるC#を使用して、インフラストラクチャのコードを全て作成できるのです。
ドメイン駆動設計を用いたサーバーレスアプリケーションの構造化
私はこれらのフレームワークの2つでサーバーレスアプリケーションを作成する方法について話しましたが、実際には、使用できるフレームワークはもっとたくさんあります。 多くの顧客が、Terraform、CloudFormation、Pulumiなどを使用してサーバーレスアプリケーションを構築しています。よく聞かれる質問は、「自分に適したものをどう選び、どうやって始めればいいのか」というものです。もちろん、私の答えは「状況次第」ですが、その決定プロセスを容易にするのに役立つ意見がいくつかあります。
.NET開発者で、最初から最後まで.NET環境を持ちたい場合は、CDKのようなコードベースのフレームワークが適しているでしょう。すでにCloudFormationを使用している会社であれば、SAMは優れた開発ツールを持っており、作成するテンプレートはインフラ担当者がすでに使用しているものとそれほど変わりません。Terraformのようなよりエンタープライズ向けのフレームワークを使用している場合は、ハイブリッドソリューションを検討することもあるかもしれません。例えば、TerraformにはCDKのアダプテーションがあり、C#コードとしてインフラストラクチャを記述し、HCLを出力することができます。最終的には、開発者とインフラ担当者の両方に適したものを見つけられるはずです。
これで、アプリケーションを構成するための基本的なツールが揃いました。アプリケーションの構築を始めると、最初は小規模かもしれません。顧客がアカウント詳細を取得する方法を提供し、次にローンの申し込みを可能にするかもしれません。そして当然、それらのローンの審査を行い、 顧客がアップロードした書類を処理する必要があります。さらに、支払いができるようにすることは絶対に必要で、支払期日が近づいたときや特別オファーがあるときに通知を送る機能も欲しいでしょう。 このように、多くの機能があり、これはおそらくアプリケーションの氷山の一角に過ぎません。
しかし、Infrastructure as Codeがあるので、すべてが素晴らしいはずですよね? ただし、注意しないと、モノリシックアプリケーションをサーバーレスに置き換えたにもかかわらず、元の状態に戻ってしまう可能性があります。テンプレートが手に負えなくなり、アプリケーションが複雑すぎて誰も実際の動作を理解できず、デプロイがリスクを伴うものになる可能性があります。これは適切な計画がない場合に直面する可能性のある実際の問題です。
この問題をより管理しやすくするために、ドメイン駆動設計と呼ばれる概念を使用できます。 これは、ビジネス機能やドメインに基づいてソフトウェアシステムをモデル化する方法論です。これはモノリシックアプリケーションにも、サーバーレスアプリのような分散アプリケーションにも適用されます。現在および将来のニーズを考えると、 アプリケーション内の論理的な境界を決定し、それぞれを独立したサーバーレスアプリケーションとして構築することができます。
ただし、やりすぎないようにしましょう。サーバーレスアプリケーションを細かく分割できるからといって、そうすべきというわけではありません。重要なのは、賢明で実用的であること、そしてドメイン駆動設計の原則を使用して、それぞれのコンポーネントとサーバーレスアプリケーションが適切なビジネス機能を果たすようにすることです。では、サーバーレスアプリケーションの適切なサイズについての推奨事項は何でしょうか?もちろん、厳密なルールはありませんが、サーバーレスアプリケーションが複数のチームにまたがる場合や、関連性の低い2つのことを行う場合は、分割の良い候補となります。
サーバーレス.NETアプリケーションのCI/CDパイプライン
さて、会場のDevOpsの方々は、おそらくこう考えているでしょう。 「これは開発者にとっては素晴らしいけど、これらのアプリケーションをデプロイするために、新しいことをたくさん学ばなければならないのか」と。でも、朗報があります。サーバーレスの.NETアプリケーションのデプロイを自動化するために、基本的に現在使用しているのと同じツールとプラクティスを使用できます。AWS CodePipeline、Azure DevOps、GitHub Actions、Jenkins、実際にはどの標準的なCI/CDツールでも使用でき、標準的な自動デプロイパターンを利用できます。
これらのツールとパターンは、サーバーレスアプリケーションに簡単に適用できます。では、典型的なサーバーレスアプリケーションのパイプラインはどのようなものでしょうか?おそらく、皆さんが慣れ親しんでいるものとそれほど変わりはないでしょう。まず、Infrastructure as Codeツールがインストールされ、初期化されていることを確認する必要があります。次に、標準的な.NETツールを使用して.NETコードをパッケージ化します。その後、サーバーレスのアーティファクトを準備します。これには、複数の環境にデプロイできるようにコンポーネントをバンドルするなどの作業が含まれます。最後に、Infrastructure as Codeツールを使用してインフラストラクチャーやサーバーレススタックをデプロイします。これにより、インフラストラクチャーとアプリケーションコードがプロビジョニングされます。
人気のあるInfrastructure as Codeツールをいくつか見て、それぞれのパイプラインを作成するために必要なものを確認してみましょう。すでに説明したAWS SAMとAWS CDK、そして非常に人気のあるInfrastructure as CodeツールであるTerraformを見ていきます。最初のステップは、Infrastructure as Codeフレームワークをインストールして初期化することです。ここで分かるように、ツールを初期化するために実行する必要があるのは、せいぜい数個のコマンドだけです。ほとんどのCI/CDツールでは、これを他のパイプラインで使用できる再利用可能なテンプレートにすることができます。
次に、.NETコードをビルドしてパッケージ化します。これには、ユニットテストやセキュリティスキャンの実行などが含まれます。ご覧のように、3つのフレームワーク間で全く同じです。なぜなら、.NET用の標準CLIツールを使用してこれを自動化しているからです。そして、サーバーレスのアーティファクトを準備します。これは各ツールで少し異なり、パラメータも異なりますが、基本的に同じことを行います。AWS SAMにはbuild、Terraformにはplan、AWS CDKにはsynthがあります。これらは最終的に、後でデプロイするためにアーティファクトを中央の場所に保存したり、パイプラインの後半でデプロイしたりするためのアーティファクトを作成します。
最後に、これらのアーティファクトをクラウドにデプロイします。各ツールでパラメータの扱いが少し異なりますが、全体的な体験は使用するツールに関係なく、かなり似通っています。AWS SAMとAWS CDKはどちらもdeployサブコマンドを持っており、これは最終的にCloudFormationスタックをデプロイします。Terraformはapplyサブコマンドを使用して、先ほど作成したプランをデプロイし、ステートファイルを更新します。以上です。これがサーバーレスアプリケーションのパイプラインを構築するために必要な4つのステップです。最後のステップは、もちろんお祝いです。なぜなら、これでサーバーレス.NETアプリケーションを繰り返しビルドしてデプロイする方法を理解し、その簡単さを実感できたからです。Jamesが示したようにコードを書き、アプリケーションをモデル化するためのInfrastructure as Codeツールを選び、パイプラインをデプロイするだけです。
まとめと今後の学習リソース
このトークでは多くの内容を取り上げましたが、ここから数点だけ覚えておいてほしいことがあります。サーバーレス.NETの開発はパラダイムシフトかもしれませんが、これらのベストプラクティスを身につければ、AWSで優れたパフォーマンスの.NETベースのサーバーレスアプリケーションを簡単に作成できるということです。私たちが話し合ったネイティブツールやサービス統合を使用して、アプリケーションをシンプルにしましょう。説明した手法を使用して、Lambda関数のパフォーマンスとコードの両方を最適化しましょう。PowertoolsやLambda annotationsなどのライブラリを採用して、シンプルさを保ちながら、これらのアプリケーションを効果的に運用するために必要な機能を維持しましょう。アプリケーション、ビジネス、チームに適したInfrastructure as Codeフレームワークを選択し、ドメイン駆動設計の原則を使用して、アプリケーションを保守しやすい方法で構造化しましょう。
.NET、AWS、そして特にサーバーレスについての素晴らしいドキュメントがたくさんあります。JamesとわたしはServerless.landで、厳選されたリストをまとめました。これのスナップショットを撮っておけば、後でこれらすべてのフォローアップとして良い読み物になるでしょう。.NET on AWS、サーバーレス、またはそれに関連することについて話したい場合は、今週の残りの期間中にAWS villageにお越しください。私の同僚と私がそこにいますので、皆さんが取り組んでいることについてお話を伺い、質問にお答えできることを楽しみにしています。
最後に、私たちは.NET on AWSでの皆さんのサポートに本当に情熱を注いでいます。アプリ内のアンケートにご記入いただけると幸いです。そのフィードバックは、今後このような講演をより良いものにするのに役立ちます。皆さんが本番環境に対応した.NETサーバーレスベースのアプリケーションを効果的に構築し、AWSにデプロイするために必要なツールを手に入れたことをお伝えできて嬉しく思います。re:Inventの残りをお楽しみください。そして、今日私たちの講演にお越しいただき、ありがとうございました。
※ こちらの記事は Amazon Bedrock を様々なタスクで利用することで全て自動で作成しています。
※ どこかの機会で記事作成の試行錯誤についても記事化する予定ですが、直近技術的な部分でご興味がある場合はTwitterの方にDMください。
Discussion