AWS examples in C# – introduction to Serverless framework

Last Updated on by

Post summary: Introduction to Serverless framework and .NET code example of a lambda function with API Gateway.

This post is part of AWS examples in C# – working with SQS, DynamoDB, Lambda, ECS series. The code used for this series of blog posts is located in aws.examples.csharp GitHub repository.

When speaking about Serverless there are two concepts and terms that need to be clarified.

Serverless architecture

Serverless architecture is an application architectural concept of the cloud, enables shifting more of your operational responsibilities to the cloud. In the current examples, AWS is used, but this is a valid concept for Azure and Google cloud. Serverless allows building and running applications and services without thinking about servers.

Serverless framework

Serverless framework is a toolset that makes deployment of serverless applications to different cloud providers extremely easy and streamlined. It supports the following cloud providers: AWS, Google Cloud, Azure, OpenWhisk, and Kubeless and following programming languages: nodeJS, Go, Python, Swift, Java, PHP, and Ruby.

AWS Lambda

AWS Lambda allows easy ramp-up of service without all the hassle to manage servers and environments. The ready code is uploaded to Lambda and automatically run. More detailed information and design considerations about AWS Lambda can be found in AWS examples in C# – working with Lambda functions post.

CloudFormation

AWS CloudFormation provides infrastructure as a code (IoC) capabilities. It defines a common language to model and provision AWS application resources. AWS resources and applications are described in YAML or JSON files, which are then provisioned by CloudFormation. This gives a single source of truth. The Serverless framework uses applications when creating the underlying AWS JSON CloudFormation templates. What Serverless framework is doing is to translate its custom YAML format to a JSON CloudFormation templates, which can be found in .serverless folder of DynamoDbServerless after the deployment to AWS is done.

Create a Serverless application

Before creating the application, Serverless needs to be installed. Installation is possible as a standalone binary or as a Node.JS package. I prefer the latter because it is much simpler.

npm install -g serverless

Creating an empty application is done with the following command:

sls create --template aws-csharp --path MyService

The command outputs a nice message:

$ sls create --template aws-csharp --path MyService
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "C:\MyService"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.61.3
 -------'

Serverless: Successfully generated boilerplate for template: "aws-csharp"

Apart from the default lambda project created by AWS tools, see AWS examples in C# – create basic Lambda function post for more details, the Serverless project is not split to src and test. It is a good idea to manually split the project in order to add tests. The configuration of Serverless projects is done in serverless.yml file. The default one is very simple, it states the provider and runtime, which is aws (Amazon) and dotnetcore2.1. The handler shows which method is being called when this lambda is invoked. In the default example, the handler is CsharpHandlers::AwsDotnetCsharp.Handler::Hello, which means CsharpHandlers is the assembly name, configured in aws-csharp.csproj file. The namespace is AwsDotnetCsharp, the class name is Handler and the method is Hello.

service: myservice

provider:
  name: aws
  runtime: dotnetcore2.1

package:
  individually: true

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

    package:
      artifact: bin/release/netcoreapp2.1/hello.zip

After the application is created, it has to be built with build.cmd or build.sh scripts.

Before deployment, AWS credential should be set:

export AWS_ACCESS_KEY_ID=KIA57FV4.....
export AWS_SECRET_ACCESS_KEY=mSgsxOWVh...
export AWS_DEFAULT_REGION=us-east-1

Deployment happens with sls deploy –region $AWS_DEFAULT_REGION command, the result of deployment command is:

Testing of the function can be done with sls invoke -f hello –region $AWS_DEFAULT_REGION command. Result of testing is:

{
	"Message": "Go Serverless v1.0! Your function executed successfully!",
	"Request": {
		"Key1": null,
		"Key2": null,
		"Key3": null
	}
}

Finally, the stack can be deleted with sls remove –region $AWS_DEFAULT_REGION command.

Use API Gateway

The default, Hello World example is pretty simple. In current examples, I have elaborated a bit on Lambda usage with API Gateway in order to expose it as a RESTful service. Two lambda functions are defined inside functions node, for movies and actions. Each has a handler node which defines where is the code that lambda function is executing. With events/http node and API Gateway endpoint is created. Each endpoint has a path and a method. Movies are using GET method, Actors are working via POST method. Both functions’ handler methods receive APIGatewayProxyRequest object and return APIGatewayProxyResponse object. The payload is found in Body string property of APIGatewayProxyRequest object.

Actors function is querying the Actors table. Mode details on the query and how it is constructed in BuildQueryRequest method can be found in Querying using the low-level interface section in AWS examples in C# – basic DynamoDB operations post.

Movies lambda function is getting the JSON document from Movies table. More details on getting the data can be found in Get item using document interface section in AWS examples in C# – basic DynamoDB operations post.

Actors lambda function has two more security features defined. One is an API Key defined with private: true setting. The request should have x-api-key header with the value which is returned by deployment command, otherwise, an HTTP status code 403 (Forbidden) is returned by API Gateway. See Run the project in AWS section in AWS examples in C# – run the solution post how to obtain the proper value of aws-examples-csharp-api-key API key. The example given here is not really scalable, more details on how to manage properly API keys can be found in Managing secrets, API keys and more with Serverless article.

The other security feature is lambda authorizer configured with authorizer: authorizer setting. The authorizer lambda function is not really doing anything significant in the examples, it uses UserManagement class to check if the Authorization header has proper value.  In the real-life, this would be a database call to get the user authorizations, not like in the examples with a dummy token. The request should have Authorization header with value Bearer validToken, otherwise, an HTTP status code 401 (Unauthorized) is returned by API Gateway.

serverless.yml

service: DynamoDbServerless

provider:
  name: aws
  runtime: dotnetcore2.1
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - dynamodb:Query
      Resource: ${env:actorsTableArn}
    - Effect: "Allow"
      Action:
        - dynamodb:DescribeTable
        - dynamodb:GetItem
      Resource: ${env:moviesTableArn}
  apiKeys:
    - aws-examples-csharp-api-key

package:
  individually: true
  artifact: bin/release/netcoreapp2.1/DynamoDbServerless.zip

functions:
  movies:
    handler: DynamoDbServerless::DynamoDbServerless.Handlers.MoviesHandler::GetMovie
    events:
      - http:
          path: movies/title/{title}
          method: get

  actors:
    handler: DynamoDbServerless::DynamoDbServerless.Handlers.ActorsHandler::QueryActors
    events:
      - http:
          path: actors/search
          method: post
          private: true
          authorizer: authorizer

  authorizer:
    handler: DynamoDbServerless::DynamoDbServerless.Handlers.AuthorizationHandler::Authorize

ActorsFunction

public async Task<APIGatewayProxyResponse> QueryActors(APIGatewayProxyRequest request, ILambdaContext context)
{
	context.Logger.LogLine($"Query request: {_jsonConverter.SerializeObject(request)}");

	var requestBody = _jsonConverter.DeserializeObject<ActorsSearchRequest>(request.Body);
	if (string.IsNullOrEmpty(requestBody.FirstName))
	{
		return new APIGatewayProxyResponse
		{
			StatusCode = (int)HttpStatusCode.BadRequest,
			Body = "FirstName is mandatory"
		};
	}
	var queryRequest = BuildQueryRequest(requestBody.FirstName, requestBody.LastName);

	var response = await _dynamoDbReader.QueryAsync(queryRequest);
	context.Logger.LogLine($"Query result: {_jsonConverter.SerializeObject(response)}");

	var queryResults = BuildActorsResponse(response);

	return new APIGatewayProxyResponse
	{
		StatusCode = (int)HttpStatusCode.OK,
		Body = _jsonConverter.SerializeObject(queryResults)
	};
}

MoviesFunction

public async Task<APIGatewayProxyResponse> GetMovie(APIGatewayProxyRequest request, ILambdaContext context)
{
	context.Logger.LogLine($"Query request: {_jsonConverter.SerializeObject(request)}");

	var title = WebUtility.UrlDecode(request.PathParameters["title"]);
	var document = await _dynamoDbReader.GetDocumentAsync(TableName, title);
	context.Logger.LogLine($"Query response: {_jsonConverter.SerializeObject(document)}");

	if (document == null)
	{
		return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.NotFound };
	}

	var movie = new Movie
	{
		Title = document["Title"],
		Genre = (MovieGenre)int.Parse(document["Genre"])
	};
	return new APIGatewayProxyResponse
	{
		StatusCode = (int)HttpStatusCode.OK,
		Body = _jsonConverter.SerializeObject(movie)
	};
}

MoviesFunction

public async Task<APIGatewayCustomAuthorizerResponse> Authorize(APIGatewayCustomAuthorizerRequest request, ILambdaContext context)
{
	context.Logger.LogLine($"Query request: {_jsonConverter.SerializeObject(request)}");

	var userInfo = await _userManager.Authorize(request.AuthorizationToken?.Replace("Bearer ", string.Empty));

	return new APIGatewayCustomAuthorizerResponse
	{
		PrincipalID = userInfo.UserId,
		PolicyDocument = new APIGatewayCustomAuthorizerPolicy
		{
			Version = "2012-10-17",
			Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>
			{
				new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement
				{
					Action = new HashSet<string> {"execute-api:Invoke"},
					Effect = userInfo.Effect.ToString(),
					Resource = new HashSet<string> { request.MethodArn }
				}
			}
		}
	};
}

UserManager

public interface IUserManager
{
	Task<UserInfo> Authorize(string token);
}

public class UserManager : IUserManager
{
	private const string ValidToken = "validToken";
	private const string UserId = "usedId";

	public async Task<UserInfo> Authorize(string token)
	{
		var userInfo = new UserInfo
		{
			UserId = UserId,
			Effect = token == ValidToken ? EffectType.Allow : EffectType.Deny
		};

		return userInfo;
	}
}

Conclusion

The Serverless framework is making deployment and maintenance of lambdas very easy. It also supports different cloud providers.

Related Posts

Read more...

AWS examples in C# – working with Lambda functions

Last Updated on by

Post summary: Iintroduction to AWS Lambda functions.

This post is part of AWS examples in C# – working with SQS, DynamoDB, Lambda, ECS series. The code used for this series of blog posts is located in aws.examples.csharp GitHub repository.

AWS Lambda

AWS Lambda allows easy ramp-up of service without all the hassle to manage servers and environments. The ready code is uploaded to Lambda and automatically run. AWS Lambda automatically scales applications by running code in response to each trigger. The code runs in parallel and processes each trigger individually, scaling precisely with the size of the workload.

Main concepts

There are several terms that need to be briefly explained to get some understanding of what AWS Lambda offers.

  • Function – A code written in a programming language, for supported runtime, that does some computational work.
  • Runtime – Allow running of functions in different programming languages, supported languages are: Node.js, Python, Ruby, Java, Go, .NET.
  • Event – A JSON formatted document that contains data for a function to process, which is converted to object by the runtime and passed to the function.
  • Concurrency – The number of requests that your function is serving at any given time. If a function is invoked, meanwhile executing another task, then another instance is provisioned, increasing the function’s concurrency.
  • Trigger – A resource or configuration that invokes a Lambda function. This includes AWS services, applications, and event source mappings.
  • Event source mapping – A resource in Lambda that reads items from a stream or queue and invokes a function.

AWS Lambda applications

Lambda is the actual name of serverless functions in AWS. Along with the lambda functions, AWS supports also a concept of an application, which is a combination of Lambda functions, event sources, and other resources that work together to perform tasks. AWS CloudFormation is used to collect application’s components into a single package that can be deployed and managed as one resource. Applications make Lambda projects portable.

CloudFormation

AWS CloudFormation provides infrastructure as a code (IoC) capabilities. It defines a common language to model and provision AWS application resources. AWS resources and applications are described in YAML or JSON files, which are then provisioned by CloudFormation. This gives a single source of truth.

API Gateway

API Gateway is a fully managed service that makes it easy to create, publish, maintain, monitor, and secure APIs. APIs act as the “front door” for applications to access data, business logic, or functionality from backend services. It is very easy to create RESTful APIs and WebSocket APIs with API Gateway. It supports traffic management, CORS support, authorization, access control, throttling, monitoring, and API version management.

API Keys

API keys are the way to create usage plans, so APIs can be given to customers as a product offering with predefined request rates and quotas. A usage plan is created in AWS and it has a throttling limit, which is basically the request rate limit that is applied to each API key that you add to the usage plan. A quota is configured to the usage plan and applied to its API keys. This is the maximum number of requests with a given API key that can be submitted within a specified time interval. API keys can be provided to API Gateway in the X-API-Key header, this is what is shown in the current examples. Another way to work with API keys is with a lambda authorizer function, which returns the API key as part of the authorization response. API Keys can be created or imported from a file. Important is that API keys are not used to manage authentication and authorization.

Access control

Access control to a REST API in API Gateway can be done with several mechanisms:

  • Resource policies
  • Standard AWS IAM roles and policies
  • IAM tags can be used together with IAM policies to control access
  • Endpoint policies for interface VPC endpoints
  • Lambda authorizers
  • Amazon Cognito user pools

Lambda (custom) authorizers

In the examples given, lambda (formerly knows as custom) authorizer is used. API Gateway uses a dedicated Lambda function to do the authorization. More details on how to use authorizers can be found in AWS examples in C# – introduction to Serverless framework post.

CloudWatch

CloudWatch is a monitoring and observability service. CloudWatch collects monitoring and operational data in the form of logs, metrics, and events, providing a unified view of AWS resources, applications, and services. CloudWatch can be used to detect anomalous behavior, set alarms, visualize logs and metrics side by side, take automated actions, troubleshoot issues. By default, AWS Lambda is logging into CloudWatch. This makes it very easy to trace lambda function issues.

Design considerations

There are some specifics that have to be taken into consideration when using lambdas. One of the benefits of lambdas is to be cost-effective. Users can select what amount of RAM to set for the lambda function when it is created. This is done with –memory-size in aws lambda create-function command, see more in AWS examples in C# – deploy with AWS CLI commands post. The default value is 128MB and CPU is allocated proportionally. Sometimes defining too low memory can end up in unexpected performance issues. This should be monitored and optimized based on specific programming language and code. Lambdas are paid per 100ms execution time, so this also should be taken into consideration when tweaking the memory setting. In terms of cost-effectiveness, it is more expensive to add more RAM in order to optimize from 100ms to 50ms execution time, because 100ms on the higher amount of RAM is being paid. It has to be analyzed how much it makes sense for the end-users. Also, another consideration is that API Gateway adds additional delay in total time for the request. CloudWatch logs cost money, so awareness is needed about how much data a lambda function is logging. More pitfalls with more details using lambdas can be found in Serverless Pitfalls: Issues With Running a Startup on AWS Lambda article.

Still, the main consideration for lambda performance is so-called cold start. If the function has not been run for a while then it needs some time for the first request to go through. I’ve seen up to 4 seconds when experimenting, although I had not really measured it. Theoretically, there is an option to ping your API at a certain amount of time to keep it “warm”. In practice, for heavy loads, AWS runs parallel instances of the lambda, in order to handle the traffic, and each new instance will have a cold start.

Create lambda

Practical examples of how to create AWS Lambda functions are available in the following posts:

Conclusion

AWS Lambda is a very convenient and easy way to create running applications with minimal overhead. There are certain design considerations such as lambda cold start that has to be taken into consideration when deciding on lambda usage.

Related Posts

Read more...

AWS examples in C# – working with SQS, DynamoDB, Lambda, ECS

Last Updated on by

Post summary: Overview of the AWS examples in C# series.

In several blog posts, I give some practical examples of how to use AWS SQS, DynamoDB, Lambda with C# code. The code used for this series of blog posts is located in aws.examples.csharp GitHub repository.

Introduction

AWS stands for Amazon Web Services, it is a subsidiary of Amazon that provides on-demand cloud computing platforms and APIs to individuals, companies, and governments, on a metered pay-as-you-go basis. AWS is one of the big cloud service providers. The others are Microsoft Azure and Google Cloud. All three cloud service providers have functions that are semantically common but differ in practical implementation. Also, every one of them has its own flavors. I have chosen to use AWS for these examples as it is something I have used before and I am most comfortable with it.

Architectural overview

In order to get a full understanding of the architecture, I have prepared this very basic diagram. It illustrates what services are there and how they communicate.

SqsReader and SqsWriter

Both are .NET Core 3.0 microservices running in docker containers. The images are uploaded in AWS ECR (Elastic Container Registry) and containers are run in AWS ECS (Elastic Container Service). SqsReader has a REST endpoint, by which an Actor or Movie can be posted. Both are pushed as a message to AWS SQS (Simple Queue Service). SqsWriter is listening to the SQS and in case of a message, it processes it. If the message is from type Actor or Movie then SqsReader saves it to the respective AWS DynamoDB tables. If the message is LogEntry, then the message is only output into SqsReader logs.

ActorsLambdaFunction and MoviessLambdaFunction

Both are .NET Core 2.1 lambda functions run in AWS Lambda. They listen to Actors and Movies DynamoDB tables changes and in case of new entries, they write to LogEntries DynamoDB table. Also, they write SQS messages from type LogEntry, which are then read by SqsReader.

ActorsServerlessLambda and MoviesServerlessLambda

Those are again lambda functions by are fully managed by the Serverless framework. They have a lambda application defined as well as Cloud Formation templates. They expose a REST API trough AWS API Gateway, by which the Actors table can be queried or a movie can be got from the Movies table.

Post in the series

This is a long series of posts describing into detail all the pieces of the architectural diagram above. Also, every aspect of the code in the repository is explained in detail in subsequent blog posts. It was a very interesting learning opportunity for me, which I would like to share. Here are the posts in the series:

Future plans

There are several topics I would like to go into as well, but there is no code yet for them into the GitHub repository. Those are:

  • AWS examples in C# – manage with Terraform
  • AWS examples in C# – use AWS Cognito for API Gateway authorizer

Conclusion

These series of posts are intended to give some basic overview of important AWS services and how to use them in C# code.

Related Posts

Read more...