The world of the Cloud was intended to be run serverless. Use services as and when you need them, scaling up and down on demand - save money. Yes, there may be a slight premium, but the use of Serverless services means that every penny counts.
Reduce the maintenance overhead, and reduce the running costs.
This is all the more key for services where there may be low traffic or even no traffic at all.
This is why AWS Lambda is so powerful. We could use the AWS Fargate service to run our Web API, however, this would still incur a cost as we would always need a single running instance to handle incoming requests.
The power of AWS Lambda is scaling. If you have an unknown traffic volume, you want this agility to cope with traffic fluctuations. The number of instances of your API will happily scale horizontally, yet vertical scaling is also very simple to achieve with small configuration changes.
Not only does this scaling amount to the amount of traffic you can deal with but scaling from a development perspective. Lamdas are easy and quick to provision and deploy, the majority of management is delegated to AWS itself, therefore more time developing features rather than system operations.
Further, the ease at which AWS Lambda will allow you to communicate with other AWS services is a win. Need DynamoDB or S3? Simple IAM permissions. Using a direct ALB or API Gateway, to route traffic, again a smooth setup.
One of the biggest concerns with using AWS Lambda for a Web API is Cold Starts. When an AWS Lambda has been inactive for a sustained period it shuts down.
A Cold Start is when there are no currently active - or warm - Lambdas to serve the request, therefore a new one must spin up. This can significantly increase the latency of an HTTP request to a cold API.
Cold Starts can be avoided by using provisioned concurrency, which allows you to configure the minimum number of dedicated Lamdas running in parallel. If you’re considering this approach, perhaps AWS Fargate would be a better option.
First off we’re going to create our infrastructure. Here, we are going to be using AWS Cloudformation as the declarative language for our infrastructure. We are going to create both the Lambda function and the API Gateway that will sit in front of the application to route traffic.
Perhaps you prefer to route traffic directly to a Lambda through an Application Load Balancer, however, we are also using the API Gateway to add an additional Auth layer.
The start of the template is merely a boilerplate before we declare the resources. We’re going to use the JSON format.
We need to declare the Handler which is specific to our application. This needs to be Namespace::FullQualifiedNameLambdaEntryPoint::FunctionHandlerAsync
.
The entry point must be FunctionHandlerAsync
as that is the public method the LambdaEntryPoint
class inherits from Amazon.Lambda.AspNetCoreServer
package.
Further here we can define our Lambda specifications, such as timeout duration and memory size.
Then we define our policy permissions for the Lambda, what services is the Lambda talking to? Is it talking to DynamoDB or S3? Another resource?
Finally, we declare our API Gateway events to tie the two resources together. This is for each endpoint the API needs to expose, along with whether they require authorisation. I like to be explicit with declaring these individually, rather than grouping them together.
Next up we’re going to define our API Gateway. Here we are going to specify our Cognito User Pool ARN to declare an authoriser, along with the required scopes.
This walkthrough is going to presume you merely have a standard Dotnet csprog
and sln
that is entirely empty.
An accompanying test project goes without saying.
The following should be the structure that we need to follow:
The package that we need to install for running a web API using AWS Lambda is Amazon.Lambda.AspNetCoreServer
. This package will give us the scaffolding to run the Lambda with three different classes to inherit as the entry point:
APIGatewayProxyFunction
APIGatewayHttpApiV2ProxyFunction
ApplicationLoadBalancerFunction
The entry points are what will run on the application startup. Similar to what you would conventionally have in Program.cs. This will:
This is the entry point that would be referred to in our CloudFormation infrastructure setup. It will be the entry point that AWS will use to run Lambda as a web service.
Notice that we are inheriting from Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
here. We will be using the IWebHostBuilder
so we will override that Init
method.
The local entry point is the entry point that we will use when running the application locally and debugging using the Lambda Test Tool - such a crucial tool for developing our application.
The Startup.cs
retains the same shape as with a conventional web API, with the configuration of the Dependency Injection container along with the HTTP request middleware pipeline.
As with the Startup.cs
there are no necessary AWS Lambda-specific changes needed to be made with regards to the controllers from a typical web API. Ensure they live in the Controllers folder as well as inheriting from ControllerBase
and you’re good to go.
Notice how the events that uses the authoriser above also has the Authorize
data annotation whereas the /anon
has the AllowAnonymous
annotation.
1- Controllers
2 - HelloController.cs
3- appsettings.json
4- appsettings.production.json
5- appsettings.development.json
6- aws-lambda-tools-defaults.json
7- ExampleSolution.sln
8- LambdaEntryPoint.cs
9- LocalEntryPoint.cs
10- serverless.template
11- Startup.cs
1{
2 "AWSTemplateFormatVersion": "2010-09-09",
3 "Transform": "AWS::Serverless-2016-10-31",
4 "Description": "The API Gateway and API Service",
5 "Parameters": {},
6 "Conditions": {},
7 "Resources": {
8 "AspNetCoreFunction": {},
9 "ExampleAPI": {}
10 }
11}
1 // AspNetCoreFunction section from above
2 "AspNetCoreFunction": {
3 "Type": "AWS::Serverless::Function",
4 "Properties": {
5 "FunctionName": "OurApiFunctionName",
6 "Handler": "Namespace::Namespace.LambdaEntryPoint::FunctionHandlerAsync",
7 "Runtime": "dotnet6",
8 "CodeUri": "",
9 "MemorySize": 256,
10 "Timeout": 30,
11 "Role": null,
12 "Policies": [
13 // required perms for running the lambda
14 // other required perms
15 ],
16 "Events": {
17 "OptionsResource": {
18 "Type": "Api",
19 "Properties": {
20 "Path": "/api/{proxy+}",
21 "Method": "OPTIONS",
22 "RestApiId": {
23 "Ref": "ExampleAPI"
24 }
25 }
26 },
27 "GetResource": {
28 "Type": "Api",
29 "Properties": {
30 "Path": "/api/hello",
31 "Method": "GET",
32 "Auth": {
33 "Authorizer": "DefaultAuthorizer"
34 },
35 "RestApiId": {
36 "Ref": "ExampleAPI"
37 }
38 }
39 },
40 "AnonResource": {
41 "Type": "Api",
42 "Properties": {
43 "Path": "/api/hello/anon",
44 "Method": "GET",
45 "RestApiId": {
46 "Ref": "ExampleAPI"
47 }
48 }
49 }
50 }
51 }
52 },
1 // ExampleAPI section from above
2 "ExampleAPI": {
3 "Type": "AWS::Serverless::Api",
4 "Properties": {
5 "StageName": "Prod",
6 "Auth": {
7 "Authorizers": {
8 "DefaultAuthorizer": {
9 "AuthType": "COGNITO_USER_POOLS",
10 "UserPoolArn": "userPoolArn",
11 "AuthorizationScopes": [
12 "RequiredScopes"
13 ]
14 }
15 }
16 }
17 }
18 }
1using Microsoft.AspNetCore.Hosting;
2using Microsoft.Extensions.Hosting;
3
4namespace Namespace
5{
6 /// <summary>
7 /// This class extends from APIGatewayProxyFunction which contains the method FunctionHandlerAsync which is the
8 /// actual Lambda function entry point. The Lambda handler field should be set to
9 ///
10 /// BlueprintBaseName.1::BlueprintBaseName.1.LambdaEntryPoint::FunctionHandlerAsync
11 /// </summary>
12 public class LambdaEntryPoint :
13
14 // The base class must be set to match the AWS service invoking the Lambda function. If not Amazon.Lambda.AspNetCoreServer
15 // will fail to convert the incoming request correctly into a valid ASP.NET Core request.
16 //
17 // API Gateway REST API -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
18 // API Gateway HTTP API payload version 1.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
19 // API Gateway HTTP API payload version 2.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction
20 // Application Load Balancer -> Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction
21 //
22 // Note: When using the AWS::Serverless::Function resource with an event type of "HttpApi" then payload version 2.0
23 // will be the default and you must make Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction the base class.
24
25 Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
26 {
27 /// <summary>
28 /// The builder has configuration, logging and Amazon API Gateway already configured. The startup class
29 /// needs to be configured in this method using the UseStartup<>() method.
30 /// </summary>
31 /// <param name="builder"></param>
32 protected override void Init(IWebHostBuilder builder)
33 {
34 builder
35 .UseContentRoot(Directory.GetCurrentDirectory())
36 .UseStartup<Startup>()
37 .UseLambdaServer();
38 }
39
40 /// <summary>
41 /// Use this override to customize the services registered with the IHostBuilder.
42 ///
43 /// It is recommended not to call ConfigureWebHostDefaults to configure the IWebHostBuilder inside this method.
44 /// Instead customize the IWebHostBuilder in the Init(IWebHostBuilder) overload.
45 /// </summary>
46 /// <param name="builder"></param>
47 protected override void Init(IHostBuilder builder)
48 {
49 }
50 }
51}
1using Microsoft.AspNetCore.Hosting;
2using Microsoft.Extensions.Hosting;
3
4namespace Namespace
5{
6 /// <summary>
7 /// The Main function can be used to run the ASP.NET Core application locally using the Kestrel webserver.
8 /// </summary>
9 public class LocalEntryPoint
10 {
11 public static void Main(string[] args)
12 {
13 CreateHostBuilder(args).Build().Run();
14 }
15
16 public static IHostBuilder CreateHostBuilder(string[] args) =>
17 Host.CreateDefaultBuilder(args)
18 .ConfigureWebHostDefaults(webBuilder =>
19 {
20 webBuilder.UseStartup<Startup>();
21 });
22 }
23}
1namespace Namespace;
2
3public class Startup
4{
5 public Startup(IConfiguration configuration)
6 {
7 Configuration = configuration;
8 }
9
10 public IConfiguration Configuration { get; }
11
12 // This method gets called by the runtime. Use this method to add services to the container
13 public void ConfigureServices(IServiceCollection services)
14 {}
15
16 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline
17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
18 {}
19}
1namespace Namespace.Controllers;
2
3[Route("api/[controller]")]
4public class HelloController : ControllerBase
5{
6 [HttpGet]
7 [Authorize]
8 public async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
9 {
10 return Ok();
11 }
12
13 [HttpGet("anon")]
14 [AllowAnonymous]
15 public async Task<IActionResult> GetAnonymousAsync(CancellationToken cancellationToken)
16 {
17 return Ok();
18 }
19}