Migrating .NET Console App Cron from GitHub Action to AWS Lamda
Tags: aws, awslambda, productivity, dotnet
Published: 2023-04-18 14:25:07

Previously, I managed to refactor the code cleaner. I want to run the workload or the cron from GitHub Action to the AWS Lambda and Event Bridge.

## Migration Steps

1. Creating Lambda Project using [Amazon.Lambda.Templates](https://docs.aws.amazon.com/lambda/latest/dg/csharp-package-cli.html). I use [Amazon.Lambda.Annotations](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Annotations/README.md) because it will be similar to my previous console program, and we can use dependency injection.
2. Setting up the code dependencies with `Amazon.Lambda.Logging.AspNetCore`, `AWSSDK.Extensions.NETCore.Setup`, `Amazon.Lambda.Core`, `Amazon.Lambda.Serialization.SystemTextJson`, and `Amazon.Lambda.Annotations`. I also add `BervProject.MergePDF.S3` as the project dependencies.
3. Setting up the Dependency Injection. I update the `ConfigureServices`.

    ```csharp
            var builder = new ConfigurationBuilder()
                                        .AddJsonFile("appsettings.json", true)
                                        .AddEnvironmentVariables();

            //// Add AWS Systems Manager as a potential provider for the configuration. This is 
            //// available with the Amazon.Extensions.Configuration.SystemsManager NuGet package.
            // builder.AddSystemsManager("/mergepdf/settings");

            var configuration = builder.Build();
            services.AddSingleton<IConfiguration>(configuration);
            services.AddLogging(logger =>
            {
                logger.AddLambdaLogger();
                logger.SetMinimumLevel(LogLevel.Debug);
            });
            services.Configure<S3Settings>(configuration.GetSection("S3"));
            services.AddAWSService<IAmazonS3>();
            services.AddScoped<IDownloader, Downloader>();
            services.AddScoped<IPdfMerger, PdfMerger>();
            services.AddScoped<IUploader, Uploader>();
            services.AddScoped<IMerger, Merger>();
    ```

4. I remove all the codes in `Functions.cs` except `Default` function.

    ```csharp
        /// <summary>
        /// Root route that provides information about the other requests that can be made.
        /// </summary>
        /// <returns></returns>
        [LambdaFunction()]
        public async Task Default(ILambdaContext context)
        {
            var generatedTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
            var destinationPath = $"merged/certificates-{generatedTimestamp}.pdf";
            var result = await _merger.Merge("certificates", destinationPath);
            context.Logger.LogInformation($"Result: {result}");
        }
    ```

5. The Lambda is ready! I want to use CDK to provision the Lambda and Event Bridge.

    ```csharp
            var role = Role.FromRoleName(this, "PDFMergeLambdaRole", "S3RoleLambda");
            var buildOption = new BundlingOptions()
            {
                Image = Runtime.DOTNET_6.BundlingImage,
                User = "root",
                OutputType = BundlingOutput.ARCHIVED,
                Command = new string[] {
                   "/bin/sh",
                    "-c",
                    " dotnet tool install -g Amazon.Lambda.Tools"+
                    " && cd BervProject.MergePDF.Lambda/src/BervProject.MergePDF.Lambda" +
                    " && dotnet build --configuration Release"+
                    " && dotnet lambda package --output-package /asset-output/function.zip"
                    }
            };
            var pdfMergerLambdaFunction = new Function(this, "PdfMerger", new FunctionProps
            {
                Runtime = Runtime.DOTNET_6,
                Timeout = Duration.Minutes(1),
                MemorySize = 512,
                Handler = "BervProject.MergePDF.Lambda::BervProject.MergePDF.Lambda.Functions_Default_Generated::Default",
                Code = Code.FromAsset(Directory.GetCurrentDirectory(), new Amazon.CDK.AWS.S3.Assets.AssetOptions
                {
                    Bundling = buildOption
                }),
                Environment = new Dictionary<string, string>
                {
                    { "S3__BucketName", System.Environment.GetEnvironmentVariable("BucketName") ?? "" }
                },
                Role = role,
            });
            var rule = new Rule(this, "CronRule", new RuleProps
            {
                Schedule = Schedule.Cron(new CronOptions
                {
                    Minute = "0",
                    Hour = "10",
                    Day = "1",
                    Month = "*",
                    Year = "*",
                }),
            });
            var lambdaFunctionTarget = new Amazon.CDK.AWS.Events.Targets.LambdaFunction(pdfMergerLambdaFunction, new LambdaFunctionProps());
            rule.AddTarget(lambdaFunctionTarget);
    ```

6. There are three components. Firstly, for the Execution Role, I use the existing role with the name S3RoleLambda. Secondly, the Lambda itself, I have some configuration to build the Lambda before packing it into a zip file and uploading it to the Lambda. Lastly, in Event Bridge, I integrate the cron rule with Lambda.

7. You can see the architecture like this.

    ![arch](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5q25c7ionym011gudxql.png)

8. For more details about the code changes, please look [here](https://github.com/bervProject/MergePDFOnline/pull/61). Thanks to Dmitry Balabanov for [helping me](https://repost.aws/questions/QU2EBYWenwSv2IdjSnGIZM0g/how-to-build-package-and-publish-net-c-lambda-functions-with-the-aws-cdk) figure out my mistake.

## Why I'm migrating to Lambda?

1. I want to have some data regarding the execution, for example, execution time and logs. I can check the metrics for the data.
2. I want to limit access to the S3 bucket only from internal AWS Services. :)

## Lessons Learn

It's a great benefit from the community AWS re:post. I will still think about my mistake because I don't understand how `BundlingOptions` works. :D Even though it could take a while for a response, depending on your question, I advise asking there.

I just realized BundlingOptions works like a docker build. :)

## Thank you

Thank you for reading. If you have some questions or feedback, feel free to comment here.

![Thanks gif](https://media.giphy.com/media/nvzxmC49CQBLE16BsK/giphy.gif)