Amazon Web Services ブログ

.NET 7 を使用して AWS Lambda でサーバーレスの .NET アプリケーションを構築する

この記事は、シニアクラウドアーキテクトのジェームス・イースタム、シニア・ソフトウェア・エンジニアのボー・ゴス、シニア・ソフトウェア・エンジニアのサミウラ・モハメッドが執筆しています。日本語訳はソリューションアーキテクトの遠藤が翻訳しました。原文はこちらです。

本日(2022 年 11 月 15 日)、AWSは、 AWS Lambda で.NET 7を実行するアプリケーションの構築とデプロイを可能にするツールのサポートを発表しました。これには、.NET 7 ネイティブ AOT (ahead-of-time) を使用してコンパイルされたアプリケーションも含まれます。.NET 7 は .NET の最新バージョンで、多くのパフォーマンス向上と最適化が行われています。

ネイティブ AOT により、.NET コードをネイティブバイナリに事前にコンパイルできるため、.NET 6 マネージドランタイムと比較してコールドスタートが最大 86% 速くなります。ネイティブ AOT は実行が速く、メモリ消費量が少ないため、Lambda のコストも削減できます。この記事では、ネイティブ AOT を使用して AWS Lambda で .NET 7 アプリケーションの実行を開始する方法について説明します。

概要

お客様が Lambda で .NET 7 を使用する方法は 2 つあります。1つ目に、Lambda は .NET 7 のベースコンテナイメージをリリースしました。これにより、お客様は .NET 7 Lambda 関数をコンテナイメージとしてビルドしてデプロイできます。2つ目に、Lambda のカスタムランタイムサポートを使用して、.NET 7 ネイティブ AOT を使用してネイティブコードにコンパイルされた Lambda 関数を実行できます。.NET 7 は長期サポート (LTS) リリースではないため、マネージドランタイムはリリースされていません。

ネイティブ AOT により .NET アプリケーションは単一のバイナリにプリコンパイルされ、JIT (Just In Time コンパイル) や .NET ランタイムが不要になります。このバイナリをカスタムランタイムで使用するには、Lambda ランタイムクライアントを含める必要があります。ランタイムクライアントは、アプリケーションコードを Lambda ランタイム API と統合します。これにより、アプリケーションコードを Lambda から呼び出すことができます。

本日発表した拡張ツールは、.NET 7 ネイティブ AOT を使用して .NET アプリケーションを構築し、カスタムランタイムを使用して Lambda にデプロイするタスクを効率化するものです。このツールは 3 つのツールで構成されています。.NET CLI の AWS Lambda エクステンション (Amazon.Lambda.Tools) には、.NET を使用して Lambda 関数をビルドおよびデプロイするためのコマンドが含まれています。.NET CLI は直接使用することができ、 Visual Studio 用 AWS Toolkit や、サーバーレスアプリケーションを構築するためのオープンソースフレームワークである AWS サーバーレスアプリケーションモデル (AWS SAM) でも使用されています。

ネイティブ AOT は特定の OS バージョン用にコードをコンパイルします。あるマシンで dotnet publish コマンドを実行すると、コンパイルされたコードはそのマシンの OS バージョンとプロセッサアーキテクチャでのみ実行が可能です。ネイティブ AOT を使用して Lambda でアプリケーションコードを実行するには、コードが Amazon Linux 2 (AL2) OS 上でコンパイルされている必要があります。新しいツールは、AL2 ベースの Docker イメージ内で Lambda 関数をコンパイルし、コンパイルされたアプリケーションをローカルのハードディスクに保存することをサポートします。

.NET 7 ネイティブ AOT による Lambda 関数の開発

このセクションでは、 Lambda 関数コードを.NET 7 ネイティブ AOT と互換性を持って開発する方法について説明します。これは、Microsoft がリリースしたネイティブ AOT の最初の GA バージョンです。トレードオフが伴うため、すべてのワークロードに適しているとは限りません。たとえば、動的なアセンブリのロードや System.Reflection.Emit ライブラリは使用できません。また、ネイティブ AOT はアプリケーションコードをトリミングするので、アプリケーションの実行に不可欠なコンポーネントを含む小さなバイナリが生成されます。

前提条件

はじめに

はじめに、.NET CLI のカスタムランタイムを使用して新しい Lambda 関数プロジェクトを作成します。

dotnet new lambda.NativeAOT -n LambdaNativeAot
cd ./LambdaNativeAot/src/LambdaNativeAot/
dotnet add package Amazon.Lambda.APIGatewayEvents
dotnet add package AWSSDK.Core

プロジェクトの設定を確認するには、LambdaNativeAot.csproj ファイルを開きます。このテンプレートでは、ターゲットフレームワークは net7.0 に設定されています。ネイティブ AOT を有効にするには、PublishAot という名前の新しいプロパティを追加し、値を true にします。この PublishAot フラグは、コンパイラがネイティブAOT コンパイルを行うために .NET SDK で必要とされる MSBuild プロパティです。

Lambda をカスタムランタイムで使用する場合、Lambda サービスはパッケージ化された ZIP ファイル内で bootstrap という名前の実行ファイルを探します。これを有効にするには、OutputType を exe に設定し、AssemblyNamebootstrap に設定します。

正しく設定された LambdaNativeAot.csproj ファイルは次のようになります。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AWSProjectType>Lambda</AWSProjectType>
    <AssemblyName>bootstrap</AssemblyName>
    <PublishAot>true</PublishAot>
  </PropertyGroup> 
  …
</Project>

ファンクションコード

カスタムランタイムで .NET を実行すると、.NET の実行可能アセンブリ機能が使用されます。そのためには、関数コードに静的な Main メソッドを定義する必要があります。Main メソッド内で、Lambda ランタイムクライアントを初期化し、Lambda イベントの処理時に使用する関数ハンドラーと JSON シリアライザーを設定する必要があります。

Amazon.Lambda.RuntimeSupport NuGet パッケージをプロジェクトに追加して、このランタイムの初期化を可能にします。LambdaBootStrapBuilder.Create() メソッドを使用して、ハンドラーと ILambdaSerializer の実装をデシリアライズするように設定します。

private static async Task Main(string[] args)
{
    Func<string, ILambdaContext, string> handler = FunctionHandler;
    await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
        .Build()
        .RunAsync();
}

アセンブリ・トリミング

ネイティブ AOT は、コンパイルされたバイナリを最適化するためにアプリケーションコードをトリミングするため、これによって 2 つの問題が発生する可能性があります。1 つ目は、デシリアライズ/シリアライズです。Newtonsoft.JsonSystem.Text.Json などの JSON を扱うための一般的な .NET ライブラリはリフレクションに依存しています。2 つ目は、まだトリミングに対応していないサードパーティのライブラリを使う場合です。コンパイラは、ライブラリが機能するために必要なライブラリの一部を削除する場合があります。ただし、どちらの問題にも解決策があります。

JSON を使った作業

ソース生成のシリアライズは、.NET 6 で導入された言語機能です。これにより、実行時のリフレクションに頼らずに、デシリアライズに必要なコードをコンパイル時に生成できます。ネイティブ AOT の欠点の 1 つは、 System.Relefection.Emit ライブラリを使用できなくなることです。ソース生成のシリアライズにより、開発者はネイティブ AOT を使用しながら JSON を操作できます。

ソースジェネレータを使用するには、 System.Text.Json.JsonSerializerContext から継承した新しい空の部分クラスを定義する必要があります。空の部分クラスに、アプリケーションでデシリアライズ/シリアライズする必要がある任意の.NET タイプの JsonSerializable 属性を追加します。

この例では、Lambda 関数は API Gateway からイベントを受信する必要があります。プロジェクトに HttpApiJsonSerializerContext という名前の新しいクラスを作成し、以下のコードをコピーします。

[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
public partial class HttpApiJsonSerializerContext : JsonSerializerContext
{
}

アプリケーションをコンパイルすると、デシリアライズ/シリアライズを実行するための静的クラス、プロパティ、およびメソッドが生成されます。

イベントの入出力が正しくシリアライズおよびデシリアライズされるように、このカスタムシリアライザーを Lambda ランタイムにも渡す必要があります。これを行うには、起動時にシリアライザーコンテキストの新しいインスタンスをランタイムに渡します。API Gateway をソースとして使用する Lambda 関数の例を次に示します。

using System.Text.Json.Serialization;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace LambdaNativeAot;
public class Function
{
    /// <summary>
    /// The main entry point for the custom runtime.
    /// </summary>
    private static async Task Main()
    {
        Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handler = FunctionHandler;
        await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<HttpApiJsonSerializerContext>())
            .Build()
            .RunAsync();
    }

    public static async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest apigProxyEvent, ILambdaContext context)
    {
        // API Handling logic here
        return new APIGatewayHttpApiV2ProxyResponse()
        {
            StatusCode = 200,
            Body = "OK"
        };
    }
}

サードパーティライブラリ

.NET コンパイラには、アプリケーションのトリミング方法を制御する機能があります。これにより、ネイティブ AOT コンパイルでは特定のアセンブリをトリミングから除外できます。これは、アプリケーションで使用しているライブラリでまだトリムに対応していないものでも、ネイティブ AOT を引き続き使用するための強力な方法です。これは、 Amazon.Lambda.ApiGatewayEvents のような Lambda イベントソースの NuGet パッケージで重要です。これを制御しない場合、 Amazon API Gateway イベントソースの C# オブジェクトがトリミングされ、実行時にシリアライズエラーが発生します。

現在、すべての .NET AWS SDK で使用されている AWSSDK.Core ライブラリもトリミングから除外する必要があります。アセンブリのトリミングを制御するには、プロジェクトルートに rd.xml という名前の新しいファイルを作成します。rd.xml 形式の詳細については、Microsoft のドキュメントを参照してください。rd.xml ファイルにアセンブリを追加すると、そのアセンブリはトリミングの対象から除外されます。

次の例には、AWSSDK.Core、API Gateway イベント、および関数ライブラリをトリミングから除外する方法の例が含まれています。

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
    <Application>
        <Assembly Name="AWSSDK.Core" Dynamic="Required All"></Assembly>
        <Assembly Name="Amazon.Lambda.APIGatewayEvents" Dynamic="Required All"></Assembly>
        <Assembly Name="bootstrap" Dynamic="Required All"></Assembly>
    </Application>
</Directives>

アセンブリを追加した後に、rd.xml ファイルを参照するよう csproj ファイルを更新する必要があります。Lambda プロジェクトの csproj ファイルを編集し、次の ItemGroup を追加します。

<ItemGroup>
  <RdXmlFile Include="rd.xml" />
</ItemGroup>

関数をコンパイルすると、アセンブリのトリミングは指定された 3 つのライブラリをスキップします。Lambda で .NET 7 ネイティブ AOT を使用している場合は、AWSSDK.Core ライブラリと、Lambda 関数が使用するイベントソースのライブラリの両方を除外することを推奨します。サーバーレスアプリケーションのトレースに AWS X-Ray SDK for .NET を使用している場合、これも除外する必要があります。

.NET 7 ネイティブ AOT アプリケーションのデプロイ

ここからは、3 つのデプロイツールを使用して、.NET 7 ネイティブ AOT 関数を Lambda でビルドし、デプロイする方法を説明します。

.NET CLI を使用する

前提条件

  • Docker (Amazon Linux 2 以外のマシンでコンパイルする場合)

ビルドとデプロイ

ネイティブ AOT でコンパイルされた Lambda 関数をパッケージ化してデプロイするには、以下を実行します。

dotnet lambda deploy-function

Lambda ツール CLI を使用して Lambda 関数コードをコンパイルおよびパッケージ化すると、ツールはプロジェクトの PublishAot フラグをチェックします。true に設定されている場合、ツールは AL2 ベースの Docker イメージを取得し、コンテナ内でコードをコンパイルします。また、ローカルファイルシステムを実行中のコンテナにマウントし、コンパイルされたバイナリをローカルファイルシステムに格納してデプロイできるようにします。デフォルトでは、生成された ZIP ファイルは bin/Release ディレクトリに出力されます。

デプロイが完了したら、次のコマンドを実行して、作成した関数を呼び出すことができます。 FUNCTION_NAME オプションをデプロイ時に入力した関数名に置き換えてください。

dotnet lambda invoke-function FUNCTION_NAME

Visual Studio 拡張機能を使用する

AWS では、AWS Toolkit for Visual Studio を使用して Visual Studio 内からネイティブ AOT ベースの Lambda 関数をコンパイルおよびデプロイするためのサポートも発表しています。

前提条件

はじめに

今回のリリースの一環として、Visual Studio 2022 を用いて AWS Lambda でネイティブ AOT を使い始めるためのテンプレートが提供されています。Visual Studio 内から、[ファイル] → [新規作成] → [プロジェクト] を選択します。Lambda .NET 7 ネイティブ AOT を検索して、ネイティブ AOT 用の新しいプロジェクトを開始します。

ビルドとデプロイ

プロジェクトを作成後、Visual Studio でプロジェクトを右クリックし、[Publish to AWS Lambda…] を選択します。

公開ウィザードのステップを完了し、アップロードを押します。Docker が作成したログメッセージは、ネイティブ AOT の関数コードをコンパイルするときに、パブリッシュウィンドウに表示されます。

これで、[Example Requests] ドロップダウンを API Gateway AWS Proxy に設定し、[Invoke] ボタンを押すことで、Visual Studio 内からデプロイされた関数を呼び出すことができます。

AWS SAM CLI を使用する

前提条件

  • Docker (AL2 以外のマシンでコンパイルする場合)
  • AWS SAM v1.6.4 以降

開始方法

AWS SAM CLI は .NET 7 ネイティブ AOT のコンパイルとデプロイをサポートしています。開始するには、新しい AWS SAM プロジェクトを初期化します。

sam init

新規プロジェクトウィザードで、以下を選択します。

  1. What template source would you like to use? 1 – AWS Quick Start Template
  2. Choose an AWS Quick start application template. 1 – Hello World example
  3. Use the most popular runtime and package type? – N
  4. Which runtime would you like to use? aot.dotnet7 (provided.al2)
  5. Enable X-Ray Tracing? N
  6. Choose a project name

複製されたプロジェクトには、Lambda にデプロイするための設定が含まれています。AWS SAM テンプレートには、「BuildMethod」という新しい AWS SAM メタデータプロパティが必要です。

HelloWorldFunction:
  Type: AWS::Serverless::Function
  Properties:
    Runtime: 'provided.al2' # // Use provided to deploy to AWS Lambda for .NET 7 native AOT
    Architectures:
      - x86_64
  Metadata:
    BuildMethod: 'dotnet7' # // But build with new build method for .NET 7 that calls into Amazon.Lambda.Tools 

ビルドとデプロイ

ガイド付きのデプロイ手順を完了して、サーバーレスアプリケーションをビルドしデプロイします。

sam build
sam deploy --guided

AWS SAM CLI は Amazon.Lambda.Tools CLI を使用して AL2 ベースの Docker イメージをプルし、コンテナ内でアプリケーションコードをコンパイルします。AWS SAM accelerate を使用すると、開発中のサーバーレスアプリケーションの更新を高速化できます。AWS CloudFormation で変更をデプロイする代わりに、直接 API をコールするため、ローカルのコードベースを変更するたびに更新が自動化されます。詳しくは AWS SAM 開発ドキュメントをご覧ください。

まとめ

AWS は Lambda で .NET 7 ネイティブ AOT をサポートするようになりました。開始方法の詳細については、Lambda 開発者ガイドをご覧ください。Lambda で .NET 7 ネイティブ AOT を使用することによるパフォーマンスの向上の詳細については、GitHub の serverless-dotnet-demo リポジトリを参照してください。

AWS Lambda 上の .NET に関するフィードバックは、.NET Lambda GitHub リポジトリの AWS .NET チームまでご連絡ください。

その他のサーバーレスの学習リソースについては、サーバーレスランドをご覧ください。

このブログはソリューションアーキテクトの遠藤が翻訳しました。原文はこちらです。