Amazon Web Services 한국 블로그

AWS Lambda 함수로 .NET 애플리케이션 개발하기

AWS Lambda 함수가 제공하는 가장 큰 이점 중 하나는 개발 작업을 기반 인프라와 격리한다는 것입니다. 이러한 격리는 코드 배포와 관리를 용이하게 하지만, 테스트와 디버깅 및 문제 진단을 위한 접근 방식을 분명히 정의해야 합니다.

이를 지원하는 방법으로 AWS 서비스를 활용한 다양한 모범 사례가 있지만 .NET 기반으로 Lambda 함수를 개발하는 경우에는 아래 네 가지 방법을 따를 수 있습니다.

  • 독립된 기능 수준의 테스트와 디버깅을 위한 단위 테스트 수행
  • AWS Serverless Application Model CLI(AWS SAM)를 사용한 로컬 통합 테스트
  • Amazon CloudWatch 로깅을 사용한 이벤트 및 오류 기록
  • AWS X-Ray의 레코딩을 사용하여 서비스 전체에 걸친 실행 추적

이 게시물에서는 현재의 UTC 시간을 반환하는 단순한 Lambda 함수를 Amazon API Gateway 기반으로 만들어진 게이트웨이에서 호출하는 데모를 수행합니다. 이 데모를 기준으로 손쉬운 디버깅, 로깅 및 추적을 위한 코드를 설계하는 방법을 소개 하겠습니다.

이전에 .NET Core 기반의 Lambda 함수를 생성한 적이 없다면 다음 게시물에서 시작하는 데 유용한 정보를 확인할 수 있습니다.

Lambda 기능 단위 테스트

.NET Core Lambda 함수를 생성하는 가장 쉬운 방법 중 하나는 .NET Core CLI를 사용하고 Lambda Empty Serverless 템플릿을 사용하여 솔루션을 생성하는 것입니다.

Lambda 템플릿을 설치하지 않았다면 다음 명령을 실행하세요.

dotnet new -i Amazon.Lambda.Templates::*

이제 템플릿을 사용하여 서버리스 프로젝트 및 단위 테스트 프로젝트를 생성한 후, 다음 명령을 실행하여 .NET Core 솔루션에 프로젝트를 추가할 수 있습니다.

dotnet new serverless.EmptyServerless -n DebuggingExample
cd DebuggingExample
dotnet new sln -n DebuggingExample\
dotnet sln DebuggingExample.sln add */*/*.csproj

아직은 추가된 코드가 없지만 단위 테스트를 실행하여 모든 항목이 정상 동작하는지 검증할 수 있습니다. 다음 명령을 실행합니다.

cd test/DebuggingExample.Tests/
dotnet test

단위 테스트를 효과적으로 수행하기 위한 주요 원칙 중 하나는 테스트 하려는 기능을 격리한 상태에서 테스트할 수 있는지 확인하는 것입니다. Lambda 함수의 비즈니스 로직과 실제 Lambda 요청을 처리하는 코드를 분리하는 것이 좋습니다.

자주 사용하는 편집기를 사용하여 ITimeProcessor.cs라는 새 파일을 src/DebuggingExample 폴더에 생성하고, 다음과 같은 기본 인터페이스를 생성합니다.

using System;

namespace DebuggingExample
{
    public interface ITimeProcessor
    {
        DateTime CurrentTimeUTC();
    }
}

그런 다음 TimeProcessor.cs 파일을 src/DebuggingExample 폴더에 새로 만듭니다. 이 파일에는 인터페이스를 구현하는 구체적인 클래스가 포함됩니다.

using System;

namespace DebuggingExample
{
    public class TimeProcessor : ITimeProcessor
    {
        public DateTime CurrentTimeUTC()
        {
            return DateTime.UtcNow;
        }
    }
} 

이제 TimeProcessorTest.cs 파일을 src/DebuggingExample.Tests 폴더에 추가합니다. 파일에는 다음 코드가 포함되어야 합니다.

using System;
using Xunit;

namespace DebuggingExample.Tests
{
    public class TimeProcessorTest
    {
        [Fact]
        public void TestCurrentTimeUTC()
        {
            // Arrange
            var processor = new TimeProcessor();
            var preTestTimeUtc = DateTime.UtcNow;

            // Act
            var result = processor.CurrentTimeUTC();

            // Assert time moves forwards 
            var postTestTimeUtc = DateTime.UtcNow;
            Assert.True(result >= preTestTimeUtc);
            Assert.True(result <= postTestTimeUtc);
        }
    }
}

이제 테스트를 실행할 준비를 마쳤습니다. test/DebuggingExample.Tests 폴더에서 다음 명령을 실행합니다.

dotnet test

Lambda 함수로 비즈니스 로직 표현

비즈니스 로직을 쓰고 테스트했으니 이제 Lambda 함수로 나타낼 수 있습니다. src/DebuggingExample/Function.cs 파일을 편집하여 CurrentTimeUTC 메서드를 호출하도록 합니다.

using System;
using System.Collections.Generic;
using System.Net;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Newtonsoft.Json;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(
typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] 

namespace DebuggingExample
{
    public class Functions
    {
        ITimeProcessor processor = new TimeProcessor();

        public APIGatewayProxyResponse Get(
APIGatewayProxyRequest request, ILambdaContext context)
        {
            var result = processor.CurrentTimeUTC();

            return CreateResponse(result);
        }

APIGatewayProxyResponse CreateResponse(DateTime? result)
{
    int statusCode = (result != null) ? 
        (int)HttpStatusCode.OK : 
        (int)HttpStatusCode.InternalServerError;

    string body = (result != null) ? 
        JsonConvert.SerializeObject(result) : string.Empty;

    var response = new APIGatewayProxyResponse
    {
        StatusCode = statusCode,
        Body = body,
        Headers = new Dictionary<string, string>
        { 
            { "Content-Type", "application/json" }, 
            { "Access-Control-Allow-Origin", "*" } 
        }
    };
    
    return response;
}
    }
}

TimeProcessor 클래스의 인스턴스가 인스턴스화된 다음 Lambda 함수의 진입점으로 사용할 Get() 메서드가 정의됩니다.

기본적으로 .NET Core Lambda 함수 핸들러는 Stream에서 입력을 찾습니다. 이 기본 동작을 재정의하려면 serializer를 선언하고 사용자 지정 요청 및 응답 유형을 사용하여 핸들러의 메서드 서명을 정의합니다.

이 프로젝트는 serverless.EmptyServerless 템플릿을 사용하여 생성되었기 때문에 이미 기본 동작을 재정의(override)합니다. 기본 동작에 대한 재정의는 Amazon.Lambda.APIGatewayEvents를 참조한 다음, 사용자 지정 serializer를 선언하는 방식으로 수행됩니다. .NET에서 사용자 지정 serializer 사용에 대한 자세한 내용은 GitHub의 AWS Lambda for .NET Core 리포지토리를 참조하시기 바랍니다.

Get()은 몇 가지 파라미터를 사용합니다.

  • APIGatewayProxyRequest 파라미터에는 Lambda 함수를 호출하는 API 게이트웨이의 요청이 포함됩니다.
  • 선택적인 ILambdaContext 파라미터에는 실행 컨텍스트의 세부 정보가 포함됩니다.

Get() 메서드는 CurrentTimeUTC()를 호출하여 비즈니스 로직에 따라 시간을 검색합니다.

마지막으로 CurrentTimeUTC()의 결과가 CreateResponse() 메서드로 전달되고, 이 메서드는 결과를 APIGatewayResponse 객체로 변환하여 호출자에게 반환합니다.

업데이트된 Lambda 함수는 더 이상 단위 테스트를 통과하지 않으므로 test/DebuggingExample.Tests/FunctionTest.cs 파일에서 TestGetMethod를 업데이트합니다. 다음 행을 제거하여 테스트를 업데이트합니다.

Assert.Equal("Hello AWS Serverless", response.Body);

업데이트 후의 FunctionTest.cs 파일은 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Amazon.Lambda.Core;
using Amazon.Lambda.TestUtilities;
using Amazon.Lambda.APIGatewayEvents;
using DebuggingExample;

namespace DebuggingExample.Tests
{
    public class FunctionTest
    {
        public FunctionTest()
        {
        }

        [Fact]
        public void TetGetMethod()
        {
            TestLambdaContext context;
            APIGatewayProxyRequest request;
            APIGatewayProxyResponse response;

            Functions functions = new Functions();

            request = new APIGatewayProxyRequest();
            context = new TestLambdaContext();
            response = functions.Get(request, context);
            Assert.Equal(200, response.StatusCode);
        }
    }
}

업데이트 이후의 정상 동작 여부를 확인하려면 test/DebuggingExample.Tests 폴더에서 다음 명령을 실행합니다.

dotnet test

AWS SAM CLI를 사용한 로컬 통합 테스트

부분 기능 수준에서는 단위 테스트를 수행하는 것이 적절한 접근 방식입니다. 그러나 API 게이트웨이와 Lambda 함수가 서로 연동되는지 테스트하려면 AWS Lambda Developer Guide에 설명된 대로 AWS SAM CLI를 설치하여 로컬에서 테스트할 수 있습니다.

런타임 환경으로부터 기능을 격리하여 테스트할 수 있는 단위 테스트와 달리, AWS SAM CLI는 로컬에서 호스팅하는 Docker 컨테이너에서 코드를 실행합니다. 또한 로컬에서 호스팅하는 API 게이트웨이 프록시를 시뮬레이션하여 컴포넌트 통합 테스트를 실행할 수 있습니다.

AWS SAM CLI를 설치한 다음, Lambda 함수를 나타내는 템플릿을 생성하여 로컬 통합 테스트를 수행할 수 있습니다. 템플릿을 생성하려면 아래 콘텐츠를 포함한 template.yaml 파일을 생성하여 DebuggingExample 디렉터리에 저장합니다.

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

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

Resources:

    DebuggingExampleFunction:
        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:
            FunctionName: DebuggingExample
			CodeUri: src/DebuggingExample/bin/Release/netcoreapp2.1/publish
            Handler: DebuggingExample::DebuggingExample.Functions::Get
            Runtime: dotnetcore2.1
            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:
                DebuggingExample:
                    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: /
                        Method: get

Outputs:

    DebuggingExampleApi:
      Description: "API Gateway endpoint URL for Prod stage for Debugging Example function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/DebuggingExample/"

    DebuggingExampleFunction:
      Description: "Debugging Example Lambda Function ARN"
      Value: !GetAtt DebuggingExampleFunction.Arn

    DebuggingExampleFunctionIamRole:
      Description: "Implicit IAM Role created for Debugging Example function"
      Value: !GetAtt DebuggingExampleFunctionRole.Arn

이제 AWS SAM CLI 템플릿이 생성되었으니 코드를 로컬에서 테스트할 수 있습니다. Lambda 함수는 API 게이트웨이에서 요청을 수신하므로 샘플 API 게이트웨이 요청을 생성합니다. 다음 명령을 실행합니다.

sam local generate-event api > testApiRequest.json

이제 다음과 같이 샘플 요청을 전달하여 DebuggingExample 코드를 로컬로 게시하고 호출할 수 있습니다.

dotnet publish -c Release
sam local invoke "DebuggingExampleFunction" --event testApiRequest.json

처음 실행할 때는 Lambda 함수를 호스팅할 컨테이너 이미지를 가져오는 데 시간이 조금 걸릴 수 있습니다. 한 번 호출한 후에는 컨테이너 이미지가 로컬로 캐시되므로 실행이 빨라집니다.

마지막으로 샘플 요청을 전송해 기능을 테스트하는 대신 로컬에서 API 게이트웨이를 실행하여 실제 API 게이트웨이 요청을 사용하여 테스트합니다.

sam local start-api

이제 브라우저에서 http://127.0.0.1:3000/으로 이동하면 API 게이트웨이를 사용해 로컬로 호스팅되는 Lambda 함수로 요청을 전송할 수 있습니다. 브라우저에서 결과를 확인합니다.

CloudWatch를 사용하여 이벤트 로깅

테스트 전략을 세운 후에는 이에 기반하여 Lambda 함수를 실행하고, 테스트하고, 디버깅해야 합니다. AWS에 함수를 배포한 이후에는 배포된 기능 활동을 기록하여 동작을 모니터링해야 합니다.

Lambda 함수에 로깅을 추가하는 가장 쉬운 방법은 CloudWatch에 이벤트를 쓰는 코드를 추가하는 것입니다. 이 작업을 수행하려면 새 메서드 LogMessage()src/DebuggingExample/Function.cs 파일에 추가합니다.

void LogMessage(ILambdaContext ctx, string msg)
{
    ctx.Logger.LogLine(
        string.Format("{0}:{1} - {2}", 
            ctx.AwsRequestId, 
            ctx.FunctionName,
            msg));
}

이 메서드는 Lambda 함수의 Get() 메서드에서 context 객체를 가져온 다음, 컨텍스트 객체의 Logger.Logline() 메서드를 호출하여 CloudWatch에 메시지를 전달합니다.

이제 Get() 메서드의 LogMessage에 호출을 추가하여 CloudWatch에서 이벤트를 기록할 수 있습니다. Try… Catch… 블록을 추가하여 예외도 함께 기록하는 것을 추천합니다.

        public APIGatewayProxyResponse Get(APIGatewayProxyRequest request, ILambdaContext context)
        {
            LogMessage(context, "Processing request started");

            APIGatewayProxyResponse response;
            try
            {
                var result = processor.CurrentTimeUTC();
                response = CreateResponse(result);

                LogMessage(context, "Processing request succeeded.");
            }
            catch (Exception ex)
            {
                LogMessage(context, string.Format("Processing request failed - {0}", ex.Message));
                response = CreateResponse(null);
            }

            return response;
        }

변경으로 인해 손상된 부분이 없는지 확인하려면 단위 테스트를 다시 실행합니다. 단위 테스트를 재 실행 하려면 다음 명령을 실행합니다.

cd test/DebuggingExample.Tests/
dotnet test

X-Ray를 사용하여 실행 추적

이제 코드가 CloudWatch에 이벤트를 기록합니다. CloudWatch는 문제 모니터링 및 진단을 위한 견고한 메커니즘을 제공합니다.

뿐만 아니라 특히 다른 서비스에 의해 호출되거나 다른 서비스를 호출하는 경우 Lambda 함수의 실행을 추적하여 성능 또는 연결 문제를 진단하기에도 유용합니다. X-Ray는 코드 실행의 분석 및 추적에 도움이 되는 다양한 기능을 제공합니다.

함수에서 트레이스 기능을 활성화하려면 이전에 생성한 SAM 템플릿을 수정하여 함수 리소스 정의에 새 속성을 추가해야 합니다. SAM을 사용하면 Tracing 속성을 추가하고 template.yaml 파일의 Globals 섹션에 있는 Timeout 특성 아래에 Active로 지정하여 쉽게 적용할 수 있습니다.

Globals:
    Function:
        Timeout: 10
        Tracing: Active

.NET Core 코드에서 X-Ray를 호출하려면, src/DebuggingExample 폴더에서 다음 명령을 실행하여 AWSSDKXRayRecoder를 솔루션에 추가해야 합니다.

dotnet add package AWSXRayRecorder –-version 2.2.1-beta

다음 using 문을 src/DebuggingExample/Function.cs 파일의 맨 위에 추가합니다.

using Amazon.XRay.Recorder.Core;

새 메서드를 Function 클래스에 추가합니다. 이 클래스는 함수와 이름을 가져온 다음, X-Ray 하위 세그먼트를 기록하여 함수의 실행을 추적합니다.

        private T TraceFunction<T>(Func<T> func, string subSegmentName)
        {
            AWSXRayRecorder.Instance.BeginSubsegment(subSegmentName);
            T result = func();
            AWSXRayRecorder.Instance.EndSubsegment();

            return result;
        } 

이제 Get() 메서드를 업데이트 합시다.

var result = processor.CurrentTimeUTC();

위의 행을 다음 행으로 바꿉니다.

var result = TraceFunction(processor.CurrentTimeUTC, "GetTime");

Function.cs의 최종 버전은 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Net;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Newtonsoft.Json;
using Amazon.XRay.Recorder.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(
typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace DebuggingExample
{
    public class Functions
    {
        ITimeProcessor processor = new TimeProcessor();

        public APIGatewayProxyResponse Get(APIGatewayProxyRequest request, ILambdaContext context)
        {
            LogMessage(context, "Processing request started");

            APIGatewayProxyResponse response;
            try
            {
                var result = TraceFunction(processor.CurrentTimeUTC, "GetTime");
                response = CreateResponse(result);

                LogMessage(context, "Processing request succeeded.");
            }
            catch (Exception ex)
            {
                LogMessage(context, string.Format("Processing request failed - {0}", ex.Message));
                response = CreateResponse(null);
            }

            return response;
        }

        APIGatewayProxyResponse CreateResponse(DateTime? result)
        {
            int statusCode = (result != null) ?
                (int)HttpStatusCode.OK :
                (int)HttpStatusCode.InternalServerError;

            string body = (result != null) ?
                JsonConvert.SerializeObject(result) : string.Empty;

            var response = new APIGatewayProxyResponse
            {
                StatusCode = statusCode,
                Body = body,
                Headers = new Dictionary<string, string>
        {
            { "Content-Type", "application/json" },
            { "Access-Control-Allow-Origin", "*" }
        }
            };

            return response;
        }

        private void LogMessage(ILambdaContext context, string message)
        {
            context.Logger.LogLine(string.Format("{0}:{1} - {2}", context.AwsRequestId, context.FunctionName, message));
        }

        private T TraceFunction<T>(Func<T> func, string actionName)
        {
            AWSXRayRecorder.Instance.BeginSubsegment(actionName);
            T result = func();
            AWSXRayRecorder.Instance.EndSubsegment();

            return result;
        }
    }
}

AWS X-Ray에는 추적 정보를 수집하는 에이전트가 필요하므로 코드를 로컬로 테스트하려면 AWS X-Ray 에이전트를 설치해야 합니다. 해당 에이전트가 설치되면 단위 테스트를 다시 실행하여 변경으로 인해 손상된 항목이 없는지 확인합니다.

cd test/DebuggingExample.Tests/
dotnet test

.NET Core에서 X-Ray를 사용하기 위한 자세한 내용은 AWS X-Ray 개발자 안내서에서 확인하시기 바랍니다. Visual Studio에서 X-Ray 지원 추가에 대한 자세한 내용은 새로운 AWS X-Ray .NET Core 지원 게시물을 참조하세요.

원격으로 Lambda 함수 배포 및 테스트

Lambda 함수를 생성하고 로컬로 테스트한 후에는 코드를 패키징하고 배포할 수 있습니다.

먼저, 코드를 배포할 Amazon S3 버킷이 필요합니다. 아직 Amazon S3 버킷이 없다면 적절한 S3 버킷을 생성합니다.

이제 .NET Lambda 함수를 패키징하고 Amazon S3에 복사할 수 있습니다.

sam package \
  --template-file template.yaml \
  --output-template debugging-example.yaml \
  --s3-bucket debugging-example-deploy

마지막으로 다음 명령을 실행하여 Lambda 함수를 배포합니다.

sam deploy \
   --template-file debugging-example.yaml \
   --stack-name DebuggingExample \
   --capabilities CAPABILITY_IAM \
   --region eu-west-1

코드가 성공적으로 배포된 후 다음 명령을 실행하여 로컬 머신에서 코드를 테스트합니다.

dotnet lambda invoke-function DebuggingExample -–region eu-west-1

Lambda 함수 진단

Lambda 함수를 실행하는 동안 AWS Management Console에 로그인하고 CloudWatch Logs로 이동하여 함수의 동작을 모니터링할 수 있습니다. CloudWatch Logs 콘솔

이제 /aws/lambda/DebuggingExample 로그 그룹을 클릭하여 Lambda 함수의 모든 기록된 로그 스트림을 볼 수 있습니다.

로그 스트림 중 하나를 열면 Get() 메서드 내에서 명시적으로 기록된 이벤트 2개를 포함하여 Lambda 함수에 대해 기록된 다양한 메시지가 표시됩니다.Lambda CloudWatch Logs

로그를 로컬에서 검토하려면 AWS SAM CLI를 통해 CloudWatch 로그를 검색하여 터미널에 로그를 표시할 수 있습니다.

sam logs -n DebuggingExample --region eu-west-1

다른 방법으로는 Test on the Lambda 콘솔에서 Test를 선택하여 Lambda 함수를 실행하는 방법이 있습니다. 이 경우, 실행 결과가 Log output 섹션에 나타납니다. Lambda 콘솔 실행

X-Ray 콘솔에서 Service Map 페이지에는 Lambda 함수의 연결에 대한 맵이 표시됩니다.

Lambda 함수는 본질적으로 독립 실행형(standalone)입니다. 그러나 Lambda 함수가 다수의 다른 서비스에 연결된 경우에는 Service Map 페이지를 통해 성능 문제를 파악하는 데 중요한 정보를 확인할 수 있습니다.X-Ray 서비스 맵

Traces 화면을 열면 기록된 모든 추적 결과를 보여주는 추적 목록이 표시됩니다. 추적 중 하나를 열면 Lambda 함수 성능의 상세 내역을 확인할 수 있습니다.

X-Ray Traces UI

결론

이 게시물에서는 .NET Core 기반의 Lambda 함수를 개발하는 방법, 단위 테스트를 사용하는 방법, AWS SAM CLI를 로컬 통합 테스트에 사용하는 방법, CloudWatch를 사용하여 이벤트를 기록하고 모니터링하는 방법과 마지막으로 X-Ray를 사용하여 Lambda 함수 실행을 추적하는 방법을 살펴 봤습니다.

이러한 기술은 Lambda 함수를 효과적으로 디버깅하고 진단하는 데 유용하게 사용할 수 있는 견고한 기반을 제공합니다. 프로덕션 워크로드의 경우, 중단 없는 탁월한 고객 경험을 제공하기 위해 진단이 핵심적인 역할을 하므로 각 서비스에 대한 자세한 내용을 좀 더 살펴보는 것을 추천합니다.

– Chris Munns;

이 글은 AWS Compute BlogDeveloping .NET Core AWS Lambda functions의 한국어 번역으로 AWS 프로페셔널 서비스팀의 연나라 컨설턴트가 감수하였습니다.