いわさです。
先日 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 build
、sam deploy
してみましょう。
今回はHello World Example
とHello 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 の採用も検討してみては如何でしょうか。