In a previous post, we looked at Terraform as an option to deploy AWS Lambda Function, as long as the number of functions is low. But with a higher number of functions, we could end up building huge scripts. And this is where Severless Framework comes in:
Serverless Framework is an open source tool available for building, packaging and deploying serverless applications across multiple cloud providers and platforms like AWS, GCP, Azure, Kubernetes, etc
The Serverless Framework helps you develop and deploy AWS Lambda functions, along with the AWS infrastructure resources they require. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of Functions and Events.
Before starting, we need to fulfill a few pre-requisites:
- Have a IAM User with programmatic access.
- Node installed on your machine.
- Install the Amazon Lambda Templates (
dotnet new -i Amazon.Lambda.Templates
) - Install the Amazon Lambda Tools (
dotnet tool install -g Amazon.Lambda.Tools
)
Installation
npm install -g serverless
Concepts
- Services: A service is the Framework's unit of organization. You can think of it as a project file.
- Functions: The code of a serverless application is deployed and executed in AWS Lambda functions.
- Events: Functions are triggered by events. Events come from other AWS resources.
- Resources: Resources are AWS infrastructure components that your functions use such as a DynamoDB table, an S3 bucket, an SNS topic, etc.
- Plugins: You can overwrite or extend the functionality using plugins.
Let's code
We will create three AWS Lambda Functions to create, get and list tasks. The task data will be stored in a DynamoBD table. Run the following command to create the .Net projects and solution:
dotnet new lambda.EmptyFunction -n PostTaskFunction -o .
dotnet new lambda.EmptyFunction -n ListTaskFunction -o .
dotnet new lambda.EmptyFunction -n GetTaskFunction -o .
dotnet new classlib -n FunctionLibrary -o src/FunctionLibrary
dotnet new sln -n serverless-framework-sandbox
dotnet sln add --in-root src/PostTaskFunction
dotnet sln add --in-root src/ListTaskFunction
dotnet sln add --in-root src/GetTaskFunction
dotnet sln add --in-root src/FunctionLibrary
Add the following NuGet packages to each project:
dotnet add src/FunctionLibrary package AWSSDK.DynamoDBv2
dotnet add src/PostTaskFunction package Amazon.Lambda.APIGatewayEvents
dotnet add src/ListTaskFunction package Amazon.Lambda.APIGatewayEvents
dotnet add src/GetTaskFunction package Amazon.Lambda.APIGatewayEvents
And the references between projects:
dotnet add src/PostTaskFunction/PostTaskFunction.csproj reference src/FunctionLibrary/FunctionLibrary.csproj
dotnet add src/ListTaskFunction/ListTaskFunction.csproj reference src/FunctionLibrary/FunctionLibrary.csproj
dotnet add src/GetTaskFunction/GetTaskFunction.csproj reference src/FunctionLibrary/FunctionLibrary.csproj
Open the solution and under the FunctionLibrary
project, add the Task.cs
file with the following content:
using Amazon.DynamoDBv2.DataModel;
namespace FunctionLibrary;
[DynamoDBTable("taskstable")]
public class Task
{
[DynamoDBHashKey("id")]
public Guid Id { get; set; }
[DynamoDBProperty("description")]
public string? Description { get; set; }
[DynamoDBProperty("title")]
public string? Title { get; set; }
}
Under the PostTaskFunction
project, modify the Function.cs
file as follow:
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace PostTaskFunction;
public class Function
{
public record RegisterTaskRequest(string Description, string Title);
public record RegisterTaskResponse(Guid Id);
private readonly AmazonDynamoDBClient client;
private readonly DynamoDBContext dbContext;
public Function()
{
client = new AmazonDynamoDBClient();
dbContext = new DynamoDBContext(client);
}
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest input, ILambdaContext context)
{
var req = JsonSerializer.Deserialize<RegisterTaskRequest>(input.Body, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true })!;
var task = new FunctionLibrary.Task() { Id = Guid.NewGuid(), Description = req.Description, Title = req.Title };
await dbContext.SaveAsync(task);
var resp = JsonSerializer.Serialize(new RegisterTaskResponse(task.Id));
return new APIGatewayProxyResponse
{
Body = resp,
StatusCode = 200,
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
}
Under the ListTaskFunction
project , modify the Function.cs
file as follow:
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace ListTaskFunction;
public class Function
{
private readonly AmazonDynamoDBClient client;
private readonly DynamoDBContext dbContext;
public Function()
{
client = new AmazonDynamoDBClient();
dbContext = new DynamoDBContext(client);
}
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest input, ILambdaContext context)
{
var tasks = await dbContext.ScanAsync<FunctionLibrary.Task>(default).GetRemainingAsync();
var resp = JsonSerializer.Serialize(tasks);
return new APIGatewayProxyResponse
{
Body = resp,
StatusCode = 200,
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
}
Under the GetTaskFunction
project, modify the Function.cs
file as follow:
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace GetTaskFunction;
public class Function
{
private readonly AmazonDynamoDBClient client;
private readonly DynamoDBContext dbContext;
public Function()
{
client = new AmazonDynamoDBClient();
dbContext = new DynamoDBContext(client);
}
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest input, ILambdaContext context)
{
var id = input.PathParameters["id"];
var task = await dbContext.LoadAsync<FunctionLibrary.Task>(new Guid(id));
if (task == null)
{
return new APIGatewayProxyResponse
{
StatusCode = 404
};
}
var resp = JsonSerializer.Serialize(task);
return new APIGatewayProxyResponse
{
Body = resp,
StatusCode = 200,
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
}
Create the packages to be deployed to AWS with the following commands:
dotnet restore src/PostTaskFunction
dotnet lambda package -pl src/PostTaskFunction --configuration Release --framework net6.0 --output-package src/PostTaskFunction/bin/Release/net6.0/PostTaskFunction.zip
dotnet restore src/ListTaskFunction
dotnet lambda package -pl src/ListTaskFunction --configuration Release --framework net6.0 --output-package src/ListTaskFunction/bin/Release/net6.0/ListTaskFunction.zip
dotnet restore src/GetTaskFunction
dotnet lambda package -pl src/GetTaskFunction --configuration Release --framework net6.0 --output-package src/GetTaskFunction/bin/Release/net6.0/GetTaskFunction.zip
And finally, create a serverless.yml
file:
service: task-app
frameworkVersion: '3'
provider:
name: aws
stage: ${opt:stage, "dev"}
region: ${opt:region, "us-east-1"}
profile: ${opt:profile, "default"}
runtime: dotnet6
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:*
Resource: 'arn:aws:dynamodb:us-east-2:*:*'
package:
individually: true
functions:
post-tasks:
handler: PostTaskFunction::PostTaskFunction.Function::FunctionHandler
package:
artifact: src/PostTaskFunction/bin/Release/net6.0/PostTaskFunction.zip
events:
- http:
path: /tasks
method: post
list-tasks:
handler: ListTaskFunction::ListTaskFunction.Function::FunctionHandler
package:
artifact: src/ListTaskFunction/bin/Release/net6.0/ListTaskFunction.zip
events:
- http:
path: /tasks
method: get
get-tasks:
handler: GetTaskFunction::GetTaskFunction.Function::FunctionHandler
package:
artifact: src/GetTaskFunction/bin/Release/net6.0/GetTaskFunction.zip
events:
- http:
path: /tasks/{id}
method: get
resources:
Resources:
tasksTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: taskstable
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
Time to deploy. Run serverless deploy
. The command is going to create three Functions, one AWS API Gateway, and one DynamoDB Table:
Deploying task-app to stage dev (us-east-1)
✔ Service deployed to stack task-app-dev (124s)
endpoints:
POST - https://y4l82ho3d5.execute-api.us-east-1.amazonaws.com/dev/tasks
GET - https://y4l82ho3d5.execute-api.us-east-1.amazonaws.com/dev/tasks
GET - https://y4l82ho3d5.execute-api.us-east-1.amazonaws.com/dev/tasks/{id}
functions:
post-tasks: task-app-dev-post-tasks (1.7 MB)
list-tasks: task-app-dev-list-tasks (1.7 MB)
get-tasks: task-app-dev-get-tasks (1.7 MB)
To clean up all the resources run serverless remove
. For more details and other command execution options, take a look at the Serverless Framework documentation.
All the code is available here. Thanks, and happy coding.