Deploying AWS Lambda Functions with Serverless Framework

Deploying AWS Lambda Functions with Serverless Framework

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.