.NET Lambda 함수 코드를 네이티브 런타임 형식으로 컴파일

.NET Lambda 함수 코드를 네이티브 런타임 형식으로 컴파일

.NET 8은 네이티브 AOT(ahead-of-time) 컴파일을 지원합니다. 네이티브 AOT를 사용하면 Lambda 함수 코드를 네이티브 런타임 형식으로 컴파일해, .NET 코드를 런타임에 컴파일할 필요가 없어집니다. 네이티브 AOT 컴파일은 .NET으로 작성한 Lambda 함수의 콜드 스타트 시간을 줄일 수 있습니다. 자세한 내용은 AWS Compute Blog의 'Introducing the .NET 8 runtime for AWS Lambda' 글을 참고하세요.

출처: AWS Lambda 개발자 안내서

본문

이 페이지의 내용:

  • Lambda 런타임
  • 사전 요구 사항
  • 시작하기
  • 직렬화(Serialization)
  • 트리밍(Trimming)
  • 문제 해결

Lambda 런타임

네이티브 AOT 컴파일로 빌드한 Lambda 함수를 배포하려면 관리형 .NET 8 Lambda 런타임을 사용하세요. 이 런타임은 x86_64와 arm64 아키텍처를 모두 지원합니다.

AOT 없이 .NET Lambda 함수를 배포하면 애플리케이션은 먼저 중간 언어(IL, Intermediate Language) 코드로 컴파일됩니다. 런타임에는 Lambda 런타임의 JIT(just-in-time) 컴파일러가 IL 코드를 필요할 때 머신 코드로 컴파일합니다. 네이티브 AOT로 사전 컴파일된 Lambda 함수는 배포 시 코드를 머신 코드로 컴파일하므로, 코드를 실행하기 전에 Lambda 런타임의 .NET 런타임이나 SDK에 의존해 코드를 컴파일할 필요가 없습니다.

AOT의 한계 중 하나는 애플리케이션 코드를 .NET 8 런타임이 쓰는 것과 같은 Amazon Linux 2023(AL2023) 운영 체제 환경에서 컴파일해야 한다는 점입니다. .NET Lambda CLI는 AL2023 이미지를 사용하는 Docker 컨테이너에서 애플리케이션을 컴파일하는 기능을 제공합니다.

크로스 아키텍처 호환성 문제를 피하려면 함수에 구성한 것과 같은 프로세서 아키텍처 환경에서 코드를 컴파일할 것을 강력히 권장합니다. 크로스 컴파일의 한계에 대해 자세히 알아보려면 Microsoft .NET 문서의 'Cross-compilation'을 참고하세요.

사전 요구 사항

네이티브 AOT를 사용하려면 함수 코드를 .NET 8 런타임과 같은 AL2023 운영 체제 환경에서 컴파일해야 합니다. 다음 섹션의 .NET CLI 명령은 AL2023 환경에서 Lambda 함수를 개발·빌드할 때 Docker를 사용합니다.

네이티브 AOT 컴파일은 .NET 8의 기능입니다. 빌드 머신에 런타임만이 아니라 .NET 8 SDK를 설치해야 합니다.

Lambda 함수를 만들 때는 Amazon.Lambda.Tools .NET Global Tools 확장을 사용합니다. Amazon.Lambda.Tools를 설치하려면 다음 명령을 실행하세요.

dotnet tool install -g Amazon.Lambda.Tools

Amazon.Lambda.Tools .NET CLI 확장에 대한 자세한 내용은 GitHub의 'AWS Extensions for .NET CLI' 저장소를 참고하세요.

Lambda 함수 코드를 생성하려면 Amazon.Lambda.Templates NuGet 패키지를 사용합니다. 이 템플릿 패키지를 설치하려면 다음 명령을 실행하세요.

dotnet new install Amazon.Lambda.Templates

시작하기

.NET Global CLI와 AWS Serverless Application Model(AWS SAM) 모두 네이티브 AOT를 사용하는 애플리케이션 빌드용 시작 템플릿을 제공합니다. 첫 네이티브 AOT Lambda 함수를 빌드하려면 다음 지침의 단계를 수행하세요.

네이티브 AOT 템플릿을 사용해 새 프로젝트를 초기화한 뒤, 생성된 .cs와 .csproj 파일이 있는 디렉터리로 이동합니다. 이 예제에서는 함수 이름을 NativeAotSample로 정합니다.

dotnet new lambda.NativeAOT -n NativeAotSample
cd ./NativeAotSample/src/NativeAotSample

네이티브 AOT 템플릿이 만든 Function.cs 파일에는 다음 함수 코드가 들어 있습니다.

using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;

namespace NativeAotSample;

public class Function
{
    /// <summary>
    /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It
    /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and
    /// the JSON serializer to use for converting Lambda JSON format to the .NET types.
    /// </summary>
    private static async Task Main()
    {
        Func<string, ILambdaContext, string> handler = FunctionHandler;
        await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
            .Build()
            .RunAsync();
    }

    /// <summary>
    /// A simple function that takes a string and does a ToUpper.
    ///
    /// To use this handler to respond to an AWS event, reference the appropriate package from
    /// https://github.com/aws/aws-lambda-dotnet#events
    /// and change the string input parameter to the desired event type. When the event type
    /// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext
    /// defined below will need the JsonSerializable updated. If the return type and event type are different then the
    /// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type.
    ///
    // When using Native AOT extra testing with the deployed Lambda functions is required to ensure
    // the libraries used in the Lambda function work correctly with Native AOT. If a runtime
    // error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe
    // code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS
    // SDK for .NET does not support trimming. This will result in a larger executable size, and still does not
    // guarantee runtime trimming errors won't be hit.
    /// </summary>
    /// <param name="input"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public static string FunctionHandler(string input, ILambdaContext context)
    {
        return input.ToUpper();
    }
}

/// <summary>
/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator.
/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur
/// from the JSON serializer unable to find the serialization information for unknown types.
/// </summary>
[JsonSerializable(typeof(string))]
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
{
    // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time
    // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for.
    // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation

네이티브 AOT는 애플리케이션을 단일 네이티브 바이너리로 컴파일합니다. 그 바이너리의 진입점은 static Main 메서드입니다. static Main 안에서 Lambda 런타임이 부트스트랩되고 FunctionHandler 메서드가 설정됩니다. 런타임 부트스트랩의 일부로 new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>()를 사용해 소스 생성 직렬 변환기(source generated serializer)가 구성됩니다.

애플리케이션을 Lambda에 배포하려면 로컬 환경에서 Docker가 실행 중인지 확인하고 다음 명령을 실행하세요.

dotnet lambda deploy-function

내부적으로 .NET Global CLI는 AL2023 Docker 이미지를 다운로드하고 실행 중인 컨테이너 안에서 애플리케이션 코드를 컴파일합니다. 컴파일된 바이너리는 Lambda에 배포되기 전에 로컬 파일 시스템으로 출력됩니다.

다음 명령을 실행해 함수를 테스트하세요. <FUNCTION_NAME>은 배포 마법사에서 함수에 정한 이름으로 바꾸세요.

dotnet lambda invoke-function <FUNCTION_NAME> --payload "hello world"

CLI의 응답에는 함수 호출의 콜드 스타트(초기화 시간)와 전체 실행 시간에 대한 성능 세부 정보가 포함됩니다.

앞의 단계로 만든 AWS 리소스를 삭제하려면 다음 명령을 실행하세요. <FUNCTION_NAME>은 배포 마법사에서 함수에 정한 이름으로 바꾸세요. 더 이상 사용하지 않는 AWS 리소스를 삭제하면 AWS 계정에 불필요한 비용이 청구되는 것을 막을 수 있습니다.

dotnet lambda delete-function <FUNCTION_NAME>

직렬화(Serialization)

네이티브 AOT로 함수를 Lambda에 배포하려면 함수 코드가 **소스 생성 직렬화(source generated serialization)**를 사용해야 합니다. 속성에 접근하기 위한 직렬화 메타데이터를 모으기 위해 런타임 리플렉션을 사용하는 대신, 소스 생성기는 애플리케이션 빌드 시 컴파일되는 C# 소스 파일을 생성합니다. 소스 생성 직렬 변환기를 올바르게 구성하려면 함수가 사용하는 모든 입력·출력 객체와 사용자 지정 타입을 포함하세요. 예를 들어 API Gateway REST API에서 이벤트를 받고 사용자 지정 Product 타입을 반환하는 Lambda 함수는 다음과 같이 정의된 직렬 변환기를 포함할 것입니다.

[JsonSerializable(typeof(APIGatewayProxyRequest))]
[JsonSerializable(typeof(APIGatewayProxyResponse))]
[JsonSerializable(typeof(Product))]
public partial class CustomSerializer : JsonSerializerContext
{
}

트리밍(Trimming)

네이티브 AOT는 바이너리를 가능한 한 작게 만들기 위해 컴파일의 일부로 애플리케이션 코드를 트리밍합니다. .NET 8 for Lambda는 이전 .NET 버전에 비해 트리밍 지원이 개선되었습니다. Lambda 런타임 라이브러리, AWS .NET SDK, .NET Lambda Annotations, 그리고 .NET 8 자체에 지원이 추가되었습니다.

이런 개선으로 빌드 시 트리밍 경고를 없앨 가능성이 생겼지만, .NET이 완전히 트림 안전(trim safe)한 적은 없습니다. 즉, 함수가 의존하는 라이브러리 일부가 컴파일 단계에서 트리밍될 수 있습니다. 다음 예제처럼 .csproj 파일에서 TrimmerRootAssemblies를 정의해 이를 관리할 수 있습니다.

<ItemGroup>
    <TrimmerRootAssembly Include="AWSSDK.Core" />
    <TrimmerRootAssembly Include="AWSXRayRecorder.Core" />
    <TrimmerRootAssembly Include="AWSXRayRecorder.Handlers.AwsSdk" />
    <TrimmerRootAssembly Include="Amazon.Lambda.APIGatewayEvents" />
    <TrimmerRootAssembly Include="bootstrap" />
    <TrimmerRootAssembly Include="Shared" />
</ItemGroup>

트림 경고를 받을 때 경고를 생성하는 클래스를 TrimmerRootAssembly에 추가한다고 해서 문제가 해결되지 않을 수 있습니다. 트림 경고는 그 클래스가 런타임까지 알 수 없는 다른 클래스에 접근하려 한다는 뜻입니다. 런타임 오류를 피하려면 이 두 번째 클래스를 TrimmerRootAssembly에 추가하세요.

트림 경고 관리에 대해 자세히 알아보려면 Microsoft .NET 문서의 'Introduction to trim warnings'를 참고하세요.

문제 해결

  • Amazon.Lambda.Tools .NET Core Global Tool 버전이 오래되었습니다. 최신 버전으로 업데이트하고 다시 시도하세요.
  • 시스템의 Docker가 Windows 컨테이너를 사용하도록 구성되어 있습니다. 네이티브 AOT 빌드 환경을 실행하려면 Linux 컨테이너로 전환하세요.

일반적인 오류에 대한 자세한 내용은 GitHub의 'AWS NativeAOT for .NET' 저장소를 참고하세요.

더 알아보기 (Learn more)