Aws-lambda-dotnet: AWS Lambda๋Š” libgdiplus๋ฅผ ์‚ฌ์šฉํ•œ System.Drawing ํ˜ธ์ถœ์„ ์ง€์›ํ•ฉ๋‹ˆ๊นŒ?

์— ๋งŒ๋“  2018๋…„ 04์›” 18์ผ  ยท  7์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: aws/aws-lambda-dotnet

์ด๋ด,

์ €๋Š” ์ตœ๊ทผ์— AWS Lambda .NET Core 2 API์—์„œ ๋ณด์•ˆ ๋ฌธ์ž ์ƒ์„ฑ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ๊ทธ๊ฒƒ์ด ํ•œ ๋ฒˆ์— ์‹คํ–‰๋˜๊ณ  ์žˆ์—ˆ๋‹ค๊ณ  ๋งน์„ธํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ ์ง€๊ธˆ์€ ๋‹จ์ˆœํžˆ ์‹คํŒจํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. .NET Core์˜ ํ˜„์žฌ System.Drawing ๊ตฌํ˜„์„ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‚ด๊ฐ€ ์ฝ๊ณ  ์žˆ๋Š” ๋ฐ”์— ๋”ฐ๋ฅด๋ฉด GDI+์— ๋Œ€ํ•œ ๋ช…์‹œ์  ์ข…์†์„ฑ์œผ๋กœ ์ธํ•ด ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ”Œ๋žซํผ ๊ฐ„ ํ˜ธํ™˜์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ์กฐ์‚ฌ๋ฅผ ํ†ตํ•ด ์›๋ž˜ Mono ํŒ€์—์„œ ๊ฐœ๋ฐœํ•œ CompatCore.System.Drawing์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋งˆ๋„ ์ด๊ฒƒ์€ GDI+์˜ Linux ๊ธฐ๋ฐ˜ ๊ตฌํ˜„์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” System.Drawing์˜ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•˜๋ฉด ๋‹ค์Œ ์˜ค๋ฅ˜/์Šคํƒ ์ถ”์ ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

System.TypeInitializationException: The type initializer for 'System.Drawing.GDIPlus' threw an exception. ---> System.DllNotFoundException: Unable to load DLL 'gdiplus': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
   at System.Drawing.GDIPlus.GdiplusStartup(UInt64& token, GdiplusStartupInput& input, GdiplusStartupOutput& output)
   at System.Drawing.GDIPlus..cctor()
   --- End of inner exception stack trace ---
   at System.Drawing.GDIPlus.GdipCreateBitmapFromScan0(Int32 width, Int32 height, Int32 stride, PixelFormat format, IntPtr scan0, IntPtr& bmp)
   at System.Drawing.Bitmap..ctor(Int32 width, Int32 height, PixelFormat format)
   at Ivy.Captcha.Services.CaptchaGenerationService.GenerateCaptchaImage(Int32 captchaCharLength, Int32 width, Int32 height)
   at IAGE.Api.Data.Services.CaptchaApiService.GenerateCaptcha() in D:\Workspaces\iam-global-education\src\Api\IAGE.Api.Data\Services\CaptchaApiService.cs:line 63
   at lambda_method.lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Elmah.Io.AspNetCore.ElmahIoMiddleware.d__6.MoveNext()

๋‹ค์Œ ์Šค๋ ˆ๋“œ์— ๋”ฐ๋ฅด๋ฉด: https://github.com/CoreCompat/CoreCompat/issues/3

runtime.linux-x64.CoreCompat.System.Drawing NuGet ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ libgdiplus DLL ์ฐธ์กฐ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ทธ๊ฒƒ์€ ๋‚˜์—๊ฒŒ ์•„๋ฌด ์†Œ์šฉ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด System.Cryptography.x509Certificates์—์„œ ๋ณธ ์œ ์‚ฌํ•œ ๋ฌธ์ œ๊ฐ€ ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ผ๋ฉฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฐธ์กฐ์˜ ๋‹ค์–‘ํ•œ ์กฐํ•ฉ์„ ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‚˜๋Š” ์ด๊ฒƒ์ด ์ž‘๋™ํ•˜๋„๋ก ํ•˜๋Š” ์กฐํ•ฉ์„ ์•„์ง ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

AWS Lambda์—์„œ System.Drawing ๊ธฐ๋Šฅ์„ ์‹œ์ž‘ํ•˜์—ฌ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋žŒ์ด ์žˆ์Šต๋‹ˆ๊นŒ?

guidance

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

์˜ˆ Magick.NET์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ž‘์—…์„ ์œ„ํ•ด ๋ช‡ ๋‹ฌ ๋™์•ˆ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์ด๋ฏธ์ง€ ์ถ•์†ŒํŒ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ž‘์„ฑํ•œ Lambda ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.S3Events" Version="1.0.2" />
    <PackageReference Include="AWSSDK.S3" Version="3.3.16.2" />
    <PackageReference Include="AWSXRayRecorder.Handlers.AwsSdk" Version="2.1.0-beta" />
    <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.4.2" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.1.1" />
  </ItemGroup>

</Project>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.XRay.Recorder.Core;
using ImageMagick;

// 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 ImageResizer
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        /// <summary>
        /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
        /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the
        /// region the Lambda function is executed in.
        /// </summary>


        public Function()
        {
            Amazon.XRay.Recorder.Handlers.AwsSdk.AWSSDKHandler.RegisterXRayForAllServices();
            S3Client = new AmazonS3Client();
        }

        /// <summary>
        /// Constructs an instance with a preconfigured S3 client. This can be used for testing the outside of the Lambda environment.
        /// </summary>
        /// <param name="s3Client"></param>
        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }

        HashSet<string> SupportedImageTypes { get; } = new HashSet<string> { ".png", ".jpg", ".jpeg" };

        /// <summary>
        /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used 
        /// to respond to S3 notifications.
        /// </summary>
        /// <param name="evnt"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task FunctionHandlerAsync(S3Event evnt, ILambdaContext context)
        {
            foreach (var record in evnt.Records)
            {
                var bucket = record.S3.Bucket.Name;
                var key = record.S3.Object.Key;

                if (key.StartsWith("thumbnails"))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is already a thumbnail");
                    continue;
                }
                if (!SupportedImageTypes.Contains(Path.GetExtension(key.ToLower())))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is not a supported image type");
                    continue;
                }

                context.Logger.LogLine($"Processing s3://{bucket}/{key}");

                MemoryStream resizedImageStream;
                using (var response = await this.S3Client.GetObjectAsync(bucket, key))
                {
                    AWSXRayRecorder.Instance.BeginSubsegment("Magick Image Resize");
                    try
                    {
                        using (MagickImageCollection collection = new MagickImageCollection(response.ResponseStream))
                        {
                            foreach (MagickImage image in collection)
                            {
                                image.Resize(200, 200);
                            }
                            context.Logger.LogLine($"   Image resized");

                            resizedImageStream = new MemoryStream();
                            collection.Write(resizedImageStream);
                            resizedImageStream.Position = 0;
                        }
                    }
                    catch(Exception e)
                    {
                        AWSXRayRecorder.Instance.AddException(e);
                        throw;
                    }
                    finally
                    {
                        AWSXRayRecorder.Instance.EndSubsegment();
                    }
                }

                var index = key.LastIndexOf('/');
                var thumbnailKey = "thumbnails/" + (index != -1 ? key.Substring(index + 1) : key);
                await this.S3Client.PutObjectAsync(new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = thumbnailKey,
                    InputStream = resizedImageStream
                });

                context.Logger.LogLine($"   Thumbnail saved to s3://{bucket}/{thumbnailKey}");
            }
        }
    }
}

๋ชจ๋“  7 ๋Œ“๊ธ€

Linux์—์„œ System.Drawing์— libgdiplus๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋“ค์—ˆ์ง€๋งŒ ์‹œ๋„ํ•˜์ง€๋Š” ์•Š์•˜์Šต๋‹ˆ๋‹ค. Lambda์—์„œ ์ด๋ฏธ์ง€ ์กฐ์ž‘์„ ์œ„ํ•ด ์ €๋Š” ์ตœ์‹  ๋ฒ„์ „์˜ Magick.NET-Q16-AnyCPU๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค . ๋‚˜๋Š” ๋‹น์‹ ์ด Captcha์— ํ•„์š”ํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋Š” ์–ด๋–ค ๊ทธ๋ฆผ๋„ ๊ทธ๋ฆฌ์ง€ ์•Š์•˜์ง€๋งŒ ๊ทธ๊ฒƒ์ด ๋ช‡ ๊ฐ€์ง€ ๊ทธ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค.

๋‚ด ๋ฌธ์ œ๋Š” libgdiplus๊ฐ€ Lambda Linux ์ธ์Šคํ„ด์Šค์˜ ๊ธฐ๋ณธ ์„ค์น˜๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์•„๋งˆ๋„ runtime.linux-x64.CoreCompat.System.Drawing์€ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ ์‹œ apt-get์„ ํ†ตํ•ด ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” NuGet ํŒจํ‚ค์ง€์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์•„๋ž˜ ๋งํฌ์—์„œ ์ด ํ”„๋กœ์ ํŠธ์˜ ์ฝ”๋“œ๋ฅผ ์ž์„ธํžˆ ์‚ดํŽด๋ณธ ํ›„ ์ด๊ฒƒ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๋˜๋Š” ์™œ ๋‚˜์—๊ฒŒ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ •ํ™•ํžˆ ์ดํ•ดํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

https://github.com/CoreCompat/CoreCompat/tree/master/native/runtime.linux-x64.CoreCompat.System.Drawing

๋‹น๋ถ„๊ฐ„์€ ๊ณ„์†ํ•ด์„œ ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Magick.NET ๋งํฌ๋Š” ์ผ๋ถ€ ๋ณด์•ˆ ๋ฌธ์ž ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์œ ํšจํ•œ ๋ฐฉ๋ฒ•์ธ ๊ฒƒ ๊ฐ™์œผ๋ฏ€๋กœ ์ด๋ฅผ ๋Œ€์ฒด ์ˆ˜๋‹จ์œผ๋กœ ์œ ์ง€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋” ๋‚˜์€ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ๊ฐœ๋ฐœ๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ด๋ฏ€๋กœ ๊ธฐ๋ณธ System.Drawing์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ดํŽด๋ณด์•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์„ค์น˜๋œ GDI+ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

์ด Magick.NET ํŒจํ‚ค์ง€๊ฐ€ Lambda์—์„œ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์„ ๋ณด์•˜์Šต๋‹ˆ๊นŒ?

์˜ˆ Magick.NET์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ž‘์—…์„ ์œ„ํ•ด ๋ช‡ ๋‹ฌ ๋™์•ˆ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์ด๋ฏธ์ง€ ์ถ•์†ŒํŒ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ž‘์„ฑํ•œ Lambda ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.S3Events" Version="1.0.2" />
    <PackageReference Include="AWSSDK.S3" Version="3.3.16.2" />
    <PackageReference Include="AWSXRayRecorder.Handlers.AwsSdk" Version="2.1.0-beta" />
    <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.4.2" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.1.1" />
  </ItemGroup>

</Project>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.XRay.Recorder.Core;
using ImageMagick;

// 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 ImageResizer
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        /// <summary>
        /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
        /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the
        /// region the Lambda function is executed in.
        /// </summary>


        public Function()
        {
            Amazon.XRay.Recorder.Handlers.AwsSdk.AWSSDKHandler.RegisterXRayForAllServices();
            S3Client = new AmazonS3Client();
        }

        /// <summary>
        /// Constructs an instance with a preconfigured S3 client. This can be used for testing the outside of the Lambda environment.
        /// </summary>
        /// <param name="s3Client"></param>
        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }

        HashSet<string> SupportedImageTypes { get; } = new HashSet<string> { ".png", ".jpg", ".jpeg" };

        /// <summary>
        /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used 
        /// to respond to S3 notifications.
        /// </summary>
        /// <param name="evnt"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task FunctionHandlerAsync(S3Event evnt, ILambdaContext context)
        {
            foreach (var record in evnt.Records)
            {
                var bucket = record.S3.Bucket.Name;
                var key = record.S3.Object.Key;

                if (key.StartsWith("thumbnails"))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is already a thumbnail");
                    continue;
                }
                if (!SupportedImageTypes.Contains(Path.GetExtension(key.ToLower())))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is not a supported image type");
                    continue;
                }

                context.Logger.LogLine($"Processing s3://{bucket}/{key}");

                MemoryStream resizedImageStream;
                using (var response = await this.S3Client.GetObjectAsync(bucket, key))
                {
                    AWSXRayRecorder.Instance.BeginSubsegment("Magick Image Resize");
                    try
                    {
                        using (MagickImageCollection collection = new MagickImageCollection(response.ResponseStream))
                        {
                            foreach (MagickImage image in collection)
                            {
                                image.Resize(200, 200);
                            }
                            context.Logger.LogLine($"   Image resized");

                            resizedImageStream = new MemoryStream();
                            collection.Write(resizedImageStream);
                            resizedImageStream.Position = 0;
                        }
                    }
                    catch(Exception e)
                    {
                        AWSXRayRecorder.Instance.AddException(e);
                        throw;
                    }
                    finally
                    {
                        AWSXRayRecorder.Instance.EndSubsegment();
                    }
                }

                var index = key.LastIndexOf('/');
                var thumbnailKey = "thumbnails/" + (index != -1 ? key.Substring(index + 1) : key);
                await this.S3Client.PutObjectAsync(new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = thumbnailKey,
                    InputStream = resizedImageStream
                });

                context.Logger.LogLine($"   Thumbnail saved to s3://{bucket}/{thumbnailKey}");
            }
        }
    }
}

์ด์ œ ๋ง‰ Magick์— ๋น ์ ธ๋“ค๊ณ  ์žˆ๊ณ  ๋ฉ‹์ง€๊ฒŒ ์ž‘๋™ํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋น„๊ต์  ๋‹ค์–‘ํ•œ ์ด๋ฏธ์ง€ ์กฐ์ž‘ API์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํฌ๋กœ์Šค ํ”Œ๋žซํผ์—์„œ ์ž˜ ์ง€์›๋˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ œ์•ˆ, ๊ฐœ๋ฐœ ๋…ธ๋ ฅ์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋ฌธ์ œ ์—†์ด ์ด ์žฅ์• ๋ฌผ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค!

์ผ๋ถ€ ์‚ฌ๋žŒ๋“ค์ด ์–ด๋–ป๊ฒŒ GDI+ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์—ˆ๋Š”์ง€ ์—ฌ์ „ํžˆ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค. ๋Ÿฐํƒ€์ž„ ์‹คํ–‰ ์ „์— ์ ์ ˆํ•œ libgdiplus ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ Lambda์— ์„ค์น˜ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค.

ImageMagick Drawables API๊ฐ€ ๋‚ด๊ฐ€ ๋ณด๊ณ  ์žˆ๋Š” AWS Lambda์™€ ํ”Œ๋žซํผ ๊ฐ„ ํ˜ธํ™˜์ด ์ ์ ˆํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  Draw() ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

ImageMagick.MagickDrawErrorException: NonconformingDrawingPrimitiveDefinition `text' @ error/draw.c/DrawImage/3402
   at ImageMagick.MagickExceptionHelper.Check(IntPtr exception)
   at ImageMagick.DrawingWand.NativeDrawingWand.Render()
   at ImageMagick.MagickImage.Draw(IEnumerable`1 drawables)
   at Ivy.Captcha.Magick.Services.MagickCaptchaGenerationService.GenerateCaptchaImage(Int32 captchaCharLength, Int32 width, Int32 height)
   at IAGE.Api.Data.Services.CaptchaApiService.GenerateCaptcha() in D:\Workspaces\iam-global-education\src\Api\IAGE.Api.Data\Services\CaptchaApiService.cs:line 63
   at lambda_method.lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Elmah.Io.AspNetCore.ElmahIoMiddleware.d__6.MoveNext()

๋‚ด๊ฐ€ ๋ฌด์—‡์„ ํ•˜๋“  Lambda์—์„œ ์ž‘๋™ํ•˜๋Š” ๊ทธ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ์„ ์–ป์„ ์ˆ˜ ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. System.Drawing(Microsoft), CoreCompat.System.Drawing(Mono) ๋ฐ ImageMagick(3rd Party)์„ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ์‹œ๋„ํ–ˆ์ง€๋งŒ ๋ชจ๋‘ ์†Œ์šฉ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๋Š” AWS Lambda๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ ์–ด๋ ค์›€์„ ๊ฒช๊ณ  ์žˆ๋Š” ๋ช‡ ๊ฐ€์ง€ ์ฃผ์š” ์‹œ์Šคํ…œ ์ข…์†์„ฑ์ด ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ณ„์† ์‹คํ—˜ํ•˜๊ณ  ์„ฑ๊ณตํ•˜๋ฉด ๋‹ค์‹œ ๋ณด๊ณ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Docker ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉ์ž ์ง€์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ํ–ฅํ›„ Lambda ๋Ÿฐํƒ€์ž„์„ ์‚ฌ์šฉ์ž ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ? ์ด ๋ชจ๋“  ๊ฒƒ์€ ๋‚ด Lambda ์ธ์Šคํ„ด์Šค์— libgdiplus๋ฅผ ์„ค์น˜ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์‹คํ–‰์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” ๋ฐ”๋ณด์ด๋ฉฐ ๋‹จ์ˆœํžˆ Google ReCaptcha๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์„ ๋•Œ AWS Lambda ์„œ๋ฒ„์—์„œ Captcha ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ง์•˜์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‚ด AWS Lambda๊ฐ€ ํ™•์ธ ์ฝ”๋“œ๋งŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์–ด๋–ค ํ˜•ํƒœ์˜ ๊ทธ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ๋„ ์ฐธ์กฐํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ์—ฌ์ „ํžˆ ๋ฏธ๋ž˜์— AWS Lambda์—์„œ ์–ด๋–ค ํ˜•ํƒœ์˜ ๊ทธ๋ฆฌ๊ธฐ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‚˜๋Š” ์ด ๋ฌธ์ œ๋ฅผ ๋’ท์ „์œผ๋กœ ๋ฏธ๋ฃจ๊ณ  ๋‹น๋ถ„๊ฐ„ ์ด ๋ฌธ์ œ๋ฅผ ๋‚ด ์ชฝ์˜ ๋”์ฐํ•œ ๋””์ž์ธ ๊ฒฐ์ •์œผ๋กœ ์น˜๋ถ€ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์•„๋งˆ๋„ Lambda์˜ ๋ฐฐํฌ ๊ณ„์ธต์€ ์–ด๋Š ์‹œ์ ์—์„œ .Net Core์—์„œ ์ ์ ˆํ•œ ์ง€์›์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๊นŒ์ง€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰