[アップデート] AWS SAM CLI の 1.110.0 でも .NET 8 がサポートされたので、マネージドランタイム上での Native AOT を使ってみた

2024.02.23

いわさです。

先日 AWS Lambda のマネージドランタイムでついに .NET 8 が使えるようになったことを紹介しました。

Lambda 側のアップデートにあわせて、今朝の AWS SAM CLI の最新リリース 1.110.0 でも .NET 8 がサポートされました。

feat: add dotnet8 support (#6429) (#6723)

本記事では AWS SAM CLI から .NET 8 関数の作成・ビルド・デプロイを行ってみました。
また、マネージドランタイムでも Native AOT が使えることがわかったので Native AOT 有効した時としていない時でコールドスタート時間を計測してみました。

AWS SAM CLI 1.110.0 以上へアップデートする

前提として AWS SAM CLI のバージョンを本日時点の最新版 1.110.0 へアップデートしておきます。

% sam --version
SAM CLI, version 1.110.0

Native AOT のテンプレートも用意されている

あとは普段の SAM CLI の使い方のとおりでsam initで関数をテンプレートから作成します。
オプション指定なしでsam initしてみるとランタイムオプションにdotnet8が追加されていることが確認出来ます。

% sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - GraphQLApi Hello World Example
        13 - Full Stack
        14 - Lambda EFS example
        15 - Hello World Example With Powertools for AWS Lambda
        16 - DynamoDB Example
        17 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]:  

Which runtime would you like to use?
        1 - aot.dotnet7 (provided.al2)
        2 - dotnet8
        3 - dotnet6
        4 - go1.x
        5 - go (provided.al2)
        6 - go (provided.al2023)
        7 - graalvm.java11 (provided.al2)
        8 - graalvm.java17 (provided.al2)
        9 - java21
        10 - java17
        11 - java11
        12 - java8.al2
        13 - nodejs20.x
        14 - nodejs18.x
        15 - nodejs16.x
        16 - python3.9
        17 - python3.8
        18 - python3.12
        19 - python3.11
        20 - python3.10
        21 - ruby3.2
        22 - rust (provided.al2)
        23 - rust (provided.al2023)
Runtime:

ランタイムオプションを指定した上でsam initしたところテンプレートは次のものが使えました。
ここで気がついたのですが、テンプレート選択後にさらに Native AOT を使うかどうか選択が出来ますね。
Native AOT というのは .NET 7 から使えるようになった機能で、JIT ではなく事前にネイティブコンパイルしておくことで実行時の JIT によるオーバーヘッドを軽減出来る仕組みです。
Lambda の場合だと特にコールドスタート時間の削減を期待することが出来ます。

全てのクイックスタートテンプレートが対応しているかわからないですが、Hello World Exampleでは Native AOT を使う場合と使わない場合を選択することが出来ました。
上述の記事を見ていただくとわかるのですが、Native AOT の初期設定はひと手間必要なので、これはありがたいですね。

% sam init --runtime dotnet8
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is cli-package.
We will proceed copying the template using cli-package.

Select your starter template
        1 - Hello World Example
        2 - Hello World Example using native AOT
Template: 2

:

sam initで作成された Native AOT テンプレートを確認してみると次のような設定値となっていました。
どうやらマネージドランタイム .NET 8 で Native AOT 使えるっぽいですね。てっきり .NET 8 になってもカスタムランタイム必要かも?と思っていたのでこれは期待出来る。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Sample SAM Template for hoge0223dotnet8

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10
    MemorySize: 512

Resources:
  HelloWorldAotFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./src/HelloWorldAot/
      Handler: bootstrap
      Runtime: dotnet8
      Architectures:
        - x86_64
      MemorySize: 512
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          PARAM1: VALUE
      Events:
        HelloWorldAot:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

:

HelloWorldAot.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AWSProjectType>Lambda</AWSProjectType>
    <AssemblyName>bootstrap</AssemblyName>
    <!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <!-- Generate native aot images during publishing to improve cold start time. -->
    <PublishAot>true</PublishAot>
		<!-- StripSymbols tells the compiler to strip debugging symbols from the final executable if we're on Linux and put them into their own file. 
		This will greatly reduce the final executable's size.-->
		<StripSymbols>true</StripSymbols>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="2.2.0" />
    <PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.10.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.0" />
    <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.0" />
  </ItemGroup>
</Project>

マネージドランタイムとして Native AOT もデプロイ可能

あとはこのままsam buildsam deployしてみましょう。
今回はHello World ExampleHello World Example using native AOTをどちらもデプロイしてみました。

マネジメントコンソールで確認してみると次のような関数構成となっていました。
やはりマネージドランタイムが使われていることが確認出来まして、Native AOT はパッケージサイズが非 Native AOT よりも大きくなっており、ハンドラが bootstrap となっています。パッケージサイズが大きくなるのは Native AOT のデメリットの一つなんですよね。

非 Native AOT

Native AOT

コールドスタート時の処理時間を計測してみる

さて、Native AOT によってコールドスタート発生時の初期化時間を軽減することが期待されます。
今回こちらも試してみましたので紹介します。

まず、ウォームスタート時の時間は次のようになっていました。
このテンプレートは API Gateway から Lambda 関数が呼び出される形となっているので cURL でエンドポイントにリクエストを送信して時間を計測してみました。

# Native AOT
 % curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.416626
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.455187
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.460408

# 非 Native AOT
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.397671
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.440831
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.357376

どちらもあまり変わらないですね。
続いてコールドスタート速度を計測してみます。
コールドスタートを発生させるために、関数呼び出し前に毎回関数の構成変更(今回はタイムアウト値)を行っています。

非 Native AOT の場合のコールドスタート発生時の時間は次のようになっていました。

% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 2.123933
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 1.634230
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 1.407865
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://c5q0rwiofj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 1.381490

Native AOT の場合のコールドスタート時間は次のようになっていました。

% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.968165
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.968708
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 0.807188
% curl -w "code: %{http_code}, time_total: %{time_total}\n" -o /dev/null -s https://qbbby9d9ja.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
code: 200, time_total: 1.000450

あー、これは結構速くなっている感じがしますね!
HelloWorld でこれなので、もう少し大きいモジュールだともっと改善が期待出来るのではないでしょうか。

さいごに

本日は AWS SAM CLI の 1.110.0 でも .NET 8 がサポートされたので、マネージドランタイム上での Native AOT を使ってみました。

「.NET も SnapStart はよ」といつも思ってはいるのですが、まだ使えない状況です。
そんな中で Native AOT は .NET Lambda のパフォーマンス改善のオプションとして有効です。

以前までは LTS ではない .NET 7 でカスタムランタイムでの実行だったので Native AOT の採用はもう少し先かなと思っていたのですが、今回マネージドランタイムで気楽に導入出来るようになったので、少数派だとは思いますが .NET Lambda を使われている方は是非 Native AOT の採用も検討してみては如何でしょうか。