AWS examples in C# - introduction to Serverless framework
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.
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
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)
};
}
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)
};
}
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 }
}
}
}
};
}
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;
}
}