Developing and Deploying a .NET Core Lambda to AWS

Yesterday's exploration brought us to the point where we had a working .NET Core feature integrated with unit and functional tests.  Today, we will focus on wrapping that feature up as an AWS Lambda and automatically deploying it.

Our Lambda project (QueueMover.Lambda) was created using the AWS Lambda Project template provided as part of the AWS Toolkit for Visual Studio 2015 (installed via Extensions and Updates).

The fundamental architectural concept is to put as much code as possible in the core project that is protected by tests; then, create a extremely thin Lambda wrapper around that code.  It should only be responsible for receiving any event and context parameters, taking care of dependency injection, and calling the core feature.  Here is the code:

 public async Task MoveHandler(ILambdaContext context)
        {
            var configSettings = new LambdaConfigSettings(context);

            var amazonSqs = new AmazonSQSClient(configSettings.SqsAccessKeyId, configSettings.SqsSecretAccessKey,
                configSettings.SqsRegion);

            var queueMover = new QueueMover(configSettings, amazonSqs);
            await queueMover.Move();
        }

Note the LambdaConfigSettings class.  It is based on the interface IConfigSettings.  This allows the unit tests to mock the configuration settings; and, the functional tests can use a standard app.config file wrapped in a FileConfigSettings object based on the same IConfigSettings interface.  Now, when we integrate the QueueMover.Move feature into a Lambda, we can provide the same configuration settings using the Environment settings contained in the context parameter.

We use those settings to set up an Amazon SQS client and pass these along to the core feature.  

That's it.  That's all the code we have outside of the core project, unprotected by tests.  Having very little code in the Lambda wrapper is a very good thing, since debugging a deployed Lambda is hard.  

Deployment

Now that the Lambda is complete, doing a manual deployment is straightforward.  Deployments can also be accomplished from the command line.  In either case, it is important to understand the role of the aws-lambda-tools-defaults.json file that comes with the Lambda project template.  

{
    "profile"     : "MyProfile",
    "region"      : "us-west-2",
    "configuration" : "Release",
    "framework"     : "netcoreapp1.0",
    "function-runtime" : "dotnetcore1.0",
    "function-memory-size" : 256,
    "function-timeout"     : 30,
    "function-handler"     : "QueueMover.Lambda::QueueMover.Lambda.LambdaHandlers::MoveHandler",
    "function-name"        : "TestQueueMover",
    "function-role"        : "arn:aws:iam::458256242382:role/service-role/execute_my_lambda",
    "environment-variables" : "\"SourceQueueName\"=\"QueueMoverSource\";\"TargetQueueName\"=\"QueueMoverTarget\";\"SqsAccessKeyId\"=\"NotGonnaTellYou\";\"SqsSecretAccessKey\"=\"NopeNotGonnaDoIt\";\"SqsRegion\"=\"us-west-1\""
}

This file has a role for both manual and automated deployments.  When you use the interactive Publish to AWS Lambda... feature, the fields are pre-populated with the values from this file.  When you use the command line interface, it will use these values unless you override them with command line parameters.  (To see your parameter options, type dotnet lambda help deploy-function in your Lambda project directory.)

Here are some notable settings from the defaults file:

 "function-handler"     : "QueueMover.Lambda::QueueMover.Lambda.LambdaHandlers::MoveHandler",

This is how you tell AWS Lambda which method to invoke.  It is made up of four parts:

  • Assembly name (i.e. project name)
  • Namespace (May be the same as the assembly name if your project isn't complex enough for a namespace heirarchy.)
  • Class name
  • Method name
"function-name"        : "TestQueueMover",

This is the name of the Lambda function that you will see in AWS.

"function-role"        : "arn:aws:iam::458256242382:role/service-role/execute_my_lambda"

This is an existing AWS IAM role.

"environment-variables" : "\"SourceQueueName\"=\"QueueMoverSource\";\"TargetQueueName\"=\"QueueMoverTarget\";\"SqsAccessKeyId\"=\"NotGonnaTellYou\";\"SqsSecretAccessKey\"=\"NopeNotGonnaDoIt\";\"SqsRegion\"=\"us-west-1\"

Use these environment variables in place of your typical .config file.

By setting this file up with the standard values you will use, your automated deployment scripts/tools can simply override the settings that are variable.

For example, I created this Powershell script:

dotnet lambda deploy-function -fn "DLQ_Reprocess_ReleaseDelta" -fh "QueueMover.Lambda::QueueMover.Lambda.LambdaHandlers::MoveHandler" -ev '"SourceQueueName"="ReleaseDelta_Dev_DLQ";"TargetQueueName"="ReleaseDelta_Dev";"SqsAccessKeyId"="NotGonnaTellYou\";\"SqsSecretAccessKey\"=\"NopeNotGonnaDoIt";"SqsRegion"="us-west-2"'
dotnet lambda deploy-function -fn "DLQ_Reprocess_ReleaseStatusChange" -fh "QueueMover.Lambda::QueueMover.Lambda.LambdaHandlers::MoveHandler" -ev '"SourceQueueName"="ReleaseStatusChange_Dev_DLQ";"TargetQueueName"="ReleaseStatusChange_Dev";"SqsAccessKeyId"="NotGonnaTellYou\";\"SqsSecretAccessKey\"=\"NopeNotGonnaDoIt";"SqsRegion"="us-west-2"'

It deploys the same QueueMover.Move function twice.  Each instance has a different name and different environment settings for the appropriate queues.  Running this script automatically sets up two Lambdas.

Of course, we need a way to change these settings to be appropriate for each deployment environment.  For this, we could make the PowerShell script accept parameters.  And/Or, we can use our deployment tool Octopus Deploy to help.  That is for a future discussion with our deployment folks.