Going serverless with .NET Core, AWS Lambda and the Serverless framework

Posted on Wednesday, 08 Nov 2017

Recently I gave a talk titled 'Going serverless with AWS Lambda' where I briefly went through what the serverless is and the architectural advantages it gives you along with the trades offs to consider. Half way through the talk I then went on to demonstrate the Serverless framework and was surprised by the number of people that are currently experimenting with AWS Lambda or Azure Functions that have never heard of it, so much so that I thought I'd write a post demonstrating its value.

What is the Serverless framework and what problem does it aim to solve?

Serverless, which I'll refer to as the Serverless framework to avoid confusion, is a cloud provider agnostic toolkit designed to aid operations around building, managing and deploying serverless components, whether full-blown serverless architectures or disparate functions (or FaaS).

To give a more concrete example, Serverless framework aims to provide developers with an interface that abstracts away the vendor's cloud specific APIs and configuration whilst simultaneously providing you with additional tooling to be able to test and deploy functions with ease, perfect for rapid feedback or being able to integrate into your CI/CD pipelines.

Let's take a look.

Getting started with .NET Core and the Serverless Framework

First of all we're going to need to install the Serverless framework:

$ npm install serverless -g

Next let's see what Serverless framework templates are currently available:

$ serverless create --help

Note: In addition to the serverless command line argument, sls is a nice short hand equivalent, producing the same results:

$ sls create --help
Template for the service. Available templates:
"aws-nodejs",
"aws-python",
"aws-python3",
"aws-groovy-gradle",
"aws-java-maven",
"aws-java-gradle",
"aws-scala-sbt",
"aws-csharp",
"aws-fsharp",
"azure-nodejs",
"openwhisk-nodejs",
"openwhisk-python",
"openwhisk-swift",
"google-nodejs"

To create our .NET Core template we use the --template command:

$ serverless create --template aws-csharp --name demo

Let's take a moment to look at the files created by the Serverless framework and go through the more note worthy ones:

$ ls -la
.
..
.gitignore
Handler.cs
aws-csharp.csproj
build.cmd
build.sh
global.json
serverless.yml

Handler.cs
Opening Handler.cs reveals it's the function that will be invoked in response to an event such as notifications, S3 updates and so forth.

//Handler.cs

[assembly:LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
namespace AwsDotnetCsharp
{
    public class Handler
    {
       public Response Hello(Request request)
       {
           return new Response("Go Serverless v1.0! Your function executed successfully!", request);
       }
    }
    ...
}

serverless.yml
This is where the magic happens. The serverless.yml file is your schema which defines the configuration of your Lambda(s) (or Azure functions) and how they interact with your wider architecture. Once configured Serverless framework generates a Cloud Formation template off of the back of this which AWS uses to provision the appropriate infrastructure.

global.json
Open global.json and you'll notice it's pinned to version 1.0.4 of the .NET Core framework, this is because as of the time of writing this .NET Core 2.0 isn't supported, though Amazon have promised support is on its way.

{
  "sdk": {
    "version": "1.0.4"
  }
}

Now, let's go ahead and create our Lambda.

Creating our .NET Core Lambda

For the purpose of this demonstration we're going to create a Lambda that's reachable via HTTP. In order to do this we're going to need to stand up an API Gateway in front of it. Normally doing this would require logging into the AWS Console and manually configuring an API Gateway, so it's a perfect example to demonstrate how Serverless framework can take care of all of a lot of heavy lifting.

Let's head over to our serverless.yml file and scroll down to the following section:

# serverless.yml
functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2
...

This part of the serverless.yml file describes the various events that our Lambda should respond to. As we're going to be using API Gateway as our method of invocation we can remove a large portion of this for clarity, then uncomment the event and its properties pertaining to http:

functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
   events:
     - http:
         path: users/create
         method: get

#    Define function environment variables here
#    environment:
#      variable2: value2
...

Creating our .NET Core C# Lambda

Because we're using HTTP as our method of invocation we need to add the Amazon.Lambda.APIGatewayEvents NuGet package to our Lambda and reference the correct request and return types, we can do that using the following .NET Core CLI command:

$ dotnet add package Amazon.Lambda.APIGatewayEvents

Now let's open our Handler.cs file and update our Lambda to return the correct response type:

public APIGatewayProxyResponse Hello(APIGatewayProxyRequest request, ILambdaContext context)
{
    // Log entries show up in CloudWatch
    context.Logger.LogLine("Example log entry\n");

    var response = new APIGatewayProxyResponse
    {
        StatusCode = (int)HttpStatusCode.OK,
        Body = "{ \"Message\": \"Hello World\" }",
        Headers = new Dictionary<string, string> {{ "Content-Type", "application/json" }}
    };

    return response;
}

Now we're set. Let's move on to deploying our Lambda.

Registering an account on AWS Lambda

If you're reading this then I assume you already have an account with AWS, if not you're going to need to head over to their registration page and sign up.

Setting AWS credentials in Serverless framework

In order to enable the Serverless framework to create Lambdas and accommodating infrastructure around our Lambdas we're going to need to set up our AWS credentials. The Serverless framework documentation does a good job of explaining how to do this, but for those that know how to generate keys in AWS you can set your credentials via the following command:

serverless config credentials --provider aws --key <Your Key> --secret <Your Secret>

Build and deploy our .NET Core Lambda

Now we're good to go!

Let's verify our setup by deploying our Lambda, this will give us an opportunity to see just how rapid the feedback cycle can be when using Serverless framework.

At this point if we weren't using the Serverless framework we'd have to manually package our Lambda up into a .zip file in a certain structure then manually log into AWS to upload our zip and create an infrastructure (in this instance API Gateway in front of our Lambda). But as we're using the Serverless framework it'll take care of all of the heavy lifting.

First let's build our .NET Core Lambda:

$ sh build.sh

or if you're on Windows:

$ build.cmd

Next we'll deploy it.

Deploying Lambdas using the Serverless framework is performed using the deploy argument. In this instance we'll set the output to be verbose using the -v flag so we can see what Serverless framework is doing:

$ serverless deploy -v

Once completed you should see output similar to the following:

$ serverless deploy -v

Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - 
...
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - demo-dev
Serverless: Stack update finished...
Service Information
service: demo
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  GET - https://b2353kdlcc.execute-api.us-east-1.amazonaws.com/dev/users/create
functions:
  hello: demo-dev-hello

Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:082958828786:function:demo-dev-hello:2
ServiceEndpoint: https://b2353kdlcc.execute-api.us-east-1.amazonaws.com/dev
ServerlessDeploymentBucketName: demo-dev-serverlessdeploymentbucket-1o4sd9lppvgfv

Now if we were to log into our AWS account and navigate to the the Cloud Formation page in the region us-east-1 (see the console output) we'd see that Serverless framework has taken care of all of the heavy lifting in spinning our stack up.

Let's navigate to the endpoint address returned in the console output which is where our Lambda can be reached.

If all went as expected we should be greeted with a successful response, awesome!

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 28
Connection: keep-alive
Date: Wed, 08 Nov 2017 02:51:33 GMT
x-amzn-RequestId: bb076390-c42f-11e7-89fc-6fcb7a11f609
X-Amzn-Trace-Id: sampled=0;root=1-5a027135-bc15ce531d1ef45e3eed7a9b
X-Cache: Miss from cloudfront
Via: 1.1 3943e81340bd903a74d536bc9599c3f3.cloudfront.net (CloudFront)
X-Amz-Cf-Id: ZDHCvVSR1DAPVUfrL8bU_IuWk3aMoAotdRKBjUIor16VcBPkIiNjNw==

{
  "Message": "Hello World"
}

In addition to invoking our Lambda manually via an HTTP request, we could also invoke it using the following Serverless framework command, where the -l flag will return any log output:

$ serverless invoke -f hello -l

{
    "statusCode": 200,
    "headers": {
        "Content-Type": "application/json"
    },
    "body": "{ \"Message\": \"Hello World\" }",
    "isBase64Encoded": false
}
--------------------------------------------------------------------
START RequestId: bbcbf6a3-c430-11e7-a2e3-132defc123e3 Version: $LATEST
Example log entry

END RequestId: bbcbf6a3-c430-11e7-a2e3-132defc123e3
REPORT RequestId: bbcbf6a3-c430-11e7-a2e3-132defc123e3	Duration: 17.02 ms	Billed Duration: 100 ms 	Memory Size: 1024 MB	Max Memory Used: 34 MB

Making modifications to our Lambda

At this point if we to make any further code changes we'd have to re-run the build script (build.sh or build.cmd depending on your platform) followed by the Serverless framework deploy function command:

$ serverless deploy function -f hello

However if we needed to modify the serverless.yml file then we'd have to deploy our infrastructure changes via the deploy command:

$ serverless deploy -v

The difference being the former is far faster as it will only deploy the source code where as the later will tear down your Cloud Formation stack and stand it back up again reflecting the changes made in your serverless.yml configuration.

Command recap

So, let's recap on the more important commands we've used:

Create our Lambda:

$ serverless create --template aws-csharp --name demo

Deploy our infrastructure and code (you must have built your Lambda before hand using one of the build scripts)

$ serverless deploy -v

Or just deploy the changes to our hello function (again, we need to have built our Lambda as above)

$ serverless deploy function -f hello

At this point we can invoke our Lambda, where -l is whether we want to include log output.

$ serverless invoke -f hello -l

If our commands were written in Python or Node then you could optionally use the invoke local command, however this isn't available for .NET Core.

$ serverless invoke local -f hello

Once finished with our demo function we can clean up after ourselves using the remove command:

$ serverless remove

Adding more Lambdas

Before we wrapping up, imagine we wanted to add more Lambdas to our project, to do this we can simply add another function name to the functions section of serverless.yml configuration file:

From this:

functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello
    events:
     - http:
         path: users/create
         method: get

To this:

functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello
    events:
     - http:
         path: users/create
         method: get
  world:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler2::World
    events:
     - http:
         path: users/
         method: get

At this point all we'd need to do is create a new Handler class (for the sake of this demonstration I called it Handler2.cs) and make sure we set the handler property in our serverless.yml configuration appropriately, we'd also need to ensure we set our new Handler2 function name to World as to match the handler address in our serverless configuration.

As the additional function will require its own set of infrastructure we would need to run the build script and then use the following command to regenerate our stack:

$ serverless deploy -v

Once deployed we're able to navigate to our second function as we do our first.

We can also deploy our functions independent of one another by supplying the appropriate function name when executing the deploy function command:

$ serverless deploy function -f world

Conclusion

Hopefully this post has given you an idea as to how the Serverless framework can help you develop and manage your functions, whether using Azure, AWS, Google or any of the other providers supported.

If you're interested in learning more about the Serverless framework then I'd highly recommend checking out their documentation (which is plentiful and very well documented).

Back