Serverless, AWS Lambda, ASP.NET Core Razor Pages

What is serverless?

Before the deep(er) dive into the details it's worth to give a try to clarify the main subject. Serverless is still a hot topic today and as always, there is no one, ultimate definition for it. For starters there are two close areas - one was firstly used in 2012 and originally was described as Backend as a Service (BaaS), which means applications that significantly or fully incorporate third-party, cloud-hosted applications and services, to manage server-side logic and state. As an example, a mobile or a single-page application perfectly describes the basics of the concept, where a client application implements the business logic and connects to and uses resources via third-party services - such as databases, authentication services...etc.

While Infrastructure as a Service (IaaS) gives us a virtualization layer to create virtual machines, install operating systems, support applications and data...etc. then in case of Platform as a Service (PaaS) the provider offers more of the application stack than IaaS providers, by adding operating systems, middleware (such as databases) and other runtimes into the cloud environment.

Cloud computing service categories

FaaS

Nowadays serverless has got a more up-to-date definition: Function as a Service (FaaS) - which implies that the server-side logic is organized into functions which are also developed by the application developers and which is triggered by e.g. events or HTTP requests, they are ephemeral and run in stateless compute containers. Google Cloud Functions, Microsoft Azure Functions and AWS Lambdas are the implementations from the cloud vendors of the "Big Three".

When using FaaS we don't need to care about the image or the hardware, we can focus on building the application logic turned into functions, directly into the cloud-hosted environment. FaaS is about running backend code without managing your own server systems or your own long-lived server applications. The provider cares about everything and technically there is no limitation for the programming language, the environment or the framework - fundamentally anything can be run in a serverless-way (in case of AWS, a Lambda function can also execute another process that is bundled with its deployment artifact, so we can actually use any language that can compile down to a Unix process).

As all the work is done by the provider, serverless solutions are literally stateless - which means the state of a FaaS application must be persisted externally, outside of the instance. Startup latency or "cold start" is also an interesting topic around serverless. It means initially there is no container started to host our functions and it takes some time to build up the first ones to be able to process the first requests. The latency depends on many variables: the programming language, the number of libraries used, the amount of code written, the configuration of the environment...etc. The opposite way is a warm start, when an already existing instance is used to respond. This "problem" has been stuck in the heads of the developers and there have been many attempts to turn cold starts to warm starts or to make the provider to keep the instances - the truth is that it cannot be managed by this way. Startup latency and cold start is a "behavior" that comes with serverless, and some may turn away from this architecture and don't even consider using it, but there are other solutions that can give us an acceptable operation.

Advantages, disadvantages

Serverless is described as an outsourcing solution as many uses the very same infrastructure therefore the operational costs are reduced. Pay what you need - it can be the biggest benefit as we only have to pay for the computing what we need and what we use. It also helps when there is inconsistent or non-deterministic traffic, when a regular environment would need auto-scaling which cannot be applied for peaks. Upscaled components would cost much more because in general these servers never deliver their maximum capable computing output. So, on demand computing also ends up in a "greener" computing. It is also clear if there are less components to be supported then it ends up in less work.
A serverless solution performs upscaling with a "cold start" without any resource planning, allocation or provisioning which is done when all the current containers are already in the middle of processing. Downscaling is also done for unused containers which are retired after a few minutes of unuse.

Serverless can also be a problem as an outsourcing solution when it comes to the control of some parts of our system. Vendors can define strong constraints over these areas which affect the whole system. Moreover, each vendor also provide their own solutions so a vendor-switch can end up changing operational tools, the code and also the architecture.
There is no in-server state as mentioned before, execution duration is limited, there is startup latency ("cold start"), and debugging of production code more or less cannot be done(debugging can be done with the Amazon.Lambda.TestTool-2.1) and even if there are a lot of operations solved by the vendor (hosting, scaling, provisioning...etc.) there are steps we need to deal with like monitoring or security.

AWS Lambda

AWS Lambda is Amazon's serverless implementation. From vendor point of view, it supports many programming languages: Java, Go, PowerShell, Node.js, C#, Python, and Ruby code (https://aws.amazon.com/lambda/faqs/). And there is also a Runtime API provided, to use any additional programming languages.

AWS Lambda history, main milestones

Nov 13, 2014 - Preview release

Jul 9, 2015 - AWS Lambda now supports invoking Lambda functions with REST-compatible clients

Dec 3, 2016 - AWS Lambda support for the .NET runtime

Jan 15, 2018 - Runtime support for .NET 2.0

Jul 9, 2018 - Support for .NET Core 2.1.0 runtime in AWS Lambda

Nov 29, 2018 - Custom Runtimes

Initially when a Lambda is invoked, the provider creates a Linux-hosted "container" (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) for the function. The details are completely hidden from us: no provisioning, no allocating, no configuring of a server or an instance. In the background AWS Lambda is using Firecracker microVMs (https://firecracker-microvm.github.io/), developed by AWS, which uses Linux Kernel-based Virtual Machines (KVM) to create microVMs with fast startup time, low memory overhead, and with a virtual machine memory barrier which enables workloads from various customers to run on the same machine, without any trade-offs to security or efficiency. See details in the following link about steps: Learning Lambda Part 8

Basic AWS Lambda types

Technically we differentiate two basic patterns: the synchronous (RequestResponse - like an HTTP call) and the asynchronous invocation (event like action that occurs when e.g. inserting new records into a database). A Web API is a good example for the synchronous invocation where an API Gateway is configured to receive and forward the HTTP requests to the Lambda functions based on some routing and mapping. It returns the responses back to the caller at the end of the invocation.

A common use-case for asynchronous invocation is file processing - a file upload from a mobile application, when the picture file is directly uploaded to an S3 bucket and it triggers an event for a Lambda function which resizes the image and save it to another S3 bucket.

There are some basic settings available for a Lambda function. The amount of memory allocated to it can be set from 128 MB to 3 GB and the timeout can be specified from 1 sec to 15 minutes. Beside the computing needs, it also serves another purpose: pricing. As the charges are calculated on-demand, more memory consumed, longer processing times needed result more costs. Based on the memory boundary there are a lot of free seconds available in the free tier and for the rest the price is specified per 100ms. (https://aws.amazon.com/lambda/pricing/)
E.g. Memory: 512 MB, Free tier per month: 800000 sec, Price per 100ms: $0.000000834

For AWS Lambda concurrent requests are limited to 1000 as a maximum under one account and region. When it is reached Lambda starts throttling the requests. Due to this limitation it is highly recommended not to share the account between production and non-production systems - otherwise e.g. a less-controlled load testing in the test environment can end up in production problems on the other side (throttling is treated as an error for synchronous calls).

ASP.NET Core Razor Pages sample

When an ASP.NET Core application is running as an AWS serverless application, the requests are processed by an API Gateway component which forward the requests to the Lambda, which is in an Amazon.Lambda.AspNetCoreServer package that marshals the requests to the ASP.NET Core hosting framework.

ASP.NET Core AWS Serverless flow

The runtime package store feature (which is a cache of the NuGet packages installed on the target deployment platform) contains the packages pre-jitted which results smaller packages and also improves the cold startup time. We can make all ASP.NET Core and EF Core packages available by adding the Microsoft.AspNetCore.All dependency, but it does not include them in the deployoment package - they are available in Lambda.

When a container is ready, the .NET Core "runtime" is launched. ASP.NET Core Razor Pages are now precompiled at publish time. This means when our serverless Razor Pages are first rendered, Lambda compute time isn’t spent compiling the Razor Pages from cshtml to machine instructions.

Development

There is an extension called AWS Toolkit for Visual Studio which is the easiest way to develop, debug and deploy AWS applications. When creating a new project from the blueprint it creates a regular-like ASP.NET Core project.

There are several blueprints we can choose from - for ASP.NET Core Razor Pages we need the "ASP.NET Core Web App" blueprint:

The Program.cs is renamed to LocalEntryPoint.cs:

    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }

And in parallel there is an entry point created for the Lambda - LambdaEntryPoint.cs (this is actually the entry point from the API Gateway):

    /// <summary>
    /// This class extends from APIGatewayProxyFunction which contains the method
    /// FunctionHandlerAsync which is the actual Lambda function entry point.
    /// The Lambda handler field should be set to
    /// 
    /// AutSoft.AspNetCoreServerless::AutSoft.AspNetCoreServerless.LambdaEntryPoint::FunctionHandlerAsync
    /// </summary>
    public class LambdaEntryPoint :
        // When using an ELB's Application Load Balancer as the event source change 
        // the base class to Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction
        Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
    {
        /// <summary>
        /// The builder has configuration, logging and Amazon API Gateway already
        /// configured. The startup class needs to be configured in this method
        /// using the UseStartup<>() method.
        /// </summary>
        /// <param name="builder"></param>
        protected override void Init(IWebHostBuilder builder)
        {
            builder
                .UseStartup<Startup>();
        }
    }

Deployment is also easy from Visual Studio: right-click on the solution and select Publish to AWS Lambda - after selecting the account profile and region, we have to specify a CloudFormation stack name and an Amazon S3 bucket - it is used to upload the package and create the stack. The settings are saved to a config file, so we can do the deployment anytime from now by the dotnet publish command.
The output parameter of the deployment (which is a CloudFormation stack creation/update) is the endpoint of the API Gateway that can be accessed by now: https://[unique_identifier].execute-api.eu-central-1.amazonaws.com/Prod

The stack looks like the following - beside the permissions and some infrastructure-related items it consists two main parts:

  • AWS:ApiGateway:RestApi
  • AWS:Lambda:Function

Basically, that's it! Only a few clicks were needed to have a working Web-application deployed, in a serverless way. Naturally it is a very basic setup, but implementing business logic, accessing other AWS services (such as DynamoDb, S3, Elasticsearch, SQS/SNS...etc.), adding logging (into CloudWatch), implementing authentication (with Cognito)...etc. is developed in the very same way as we would do for a non-serverless solution.

Conclusion

There is no silver bullet. Serverless is not the magic carpet that takes us to the place where all our problems are solved. As on-premise solutions versus cloud-based ones have their own purpose, we should not expect everything from serverless against regular, VM- and cloud-based solutions. Despite this it is important to understand the specialties, the advantages and disadvantages of this kind of architectures and keep them in our minds. If we do so, we can apply them in the right business situation.

Sources

https://martinfowler.com/articles/serverless.html
https://searchcloudcomputing.techtarget.com/definition/Platform-as-a-Service-PaaS
https://aws.amazon.com/lambda/faqs/
https://aws.amazon.com/blogs/developer/serverless-asp-net-core-2-0-applications/
https://aws.amazon.com/blogs/developer/running-serverless-asp-net-core-web-apis-with-amazon-lambda/
https://medium.com/@zaccharles/making-net-aws-lambda-functions-start-10x-faster-using-lambdanative-8e53d6f12c9c

https://martinfowler.com/bliki/Serverless.html
https://medium.com/@dimoss/asp-net-core-2-2-3-0-serverless-web-apis-in-aws-lambda-with-a-custom-runtime-and-lambda-warmer-ce19ce2e2c74
https://visualstudiomagazine.com/articles/2019/02/19/aws-aspnet-core.aspx
https://visualstudiomagazine.com/articles/2019/03/20/aws-lambda.aspx
https://blog.symphonia.io/learning-lambda-1f25af64161c