What is Serverless?

“Serverless or serverless computing is a cloud-based execution model in which cloud service providers provision on-demand machine resources and manage the servers by themselves instead of customers or developers. It is a way that combines services, strategies, and practices to help developers build cloud-based apps by letting them focus on their code rather than server management.” - Amrita Pathak, Geekflare

In other words serverless does’t mean there is no servers, the server still exists in the background but it is managed by a cloud provider and abstracted away from the developer process. There are two main overlapping services in Serverless. FaaS or Function as a Service and BaaS backend as a service.

What is FaaS?

FaaS or Function as a Service is deployment of code with all the software infrastructure and provided, the developer only provides the code for the actual application. The model of payment differs from normal pay per minute/hour, if FaaS its more normal to pay per execution/request or by computing.

Calculator function

This blog posts assignment is to write and deploy an Azure Function (Http Trigger function) that takes two number inputs, validates them an then returns the sum of the inputs if they are valid inputs. You can see the entire source code in my public repo.

Set-Up

I started out trying writing a function in the web interface in an scripting version of .NET-Core, this didn’t work out for me and later I realized that I used .NET-5 syntax for C# where you are allowed to set primitive datatypes to null. I started all over locally with vs code and tried different project setups where I landed in a .NET-core 3 (which is LTS in Azure Functions), with an x-unit test project. VS Code informed me about the C# code that wasn’t available in .NET Core.

Function Source Code

public static class HttpTrigger
{
public static readonly string DefaultResponse = "Add query parameters ?a=1&b=3 to use the calculator.";
public static string responseCalculation(string decimalA, string decimalB) => $"{Convert.ToDecimal(decimalA) + Convert.ToDecimal(decimalB)}";
public static string ValidateInput(string input)
{
    //Null check
    if (input == null) return null;

    //Check for decimal type overflow
    if (input.Length > 20) return null;

    //At least one digit
    if (input.ToList().Aggregate(0, (acc, c) => 
        char.IsDigit(c) ? acc + 1 : acc + 0) == 0) return null;

    // Only digits and "." or "-"
    if (input.ToList().TrueForAll(c => 
        !char.IsDigit(c) && c != '.' && c != '-')) return null;

    //Only one "."
    if (input.ToList().Aggregate(0, (acc, x) =>
        x == '.' ? acc + 1 : acc + 0) > 1) return null;

    //dot is not first
    if (input.ToList().Aggregate(0, (acc, x) =>
        x == '.' ? acc + 1 : acc + 0) == 1 && input[0] == '.') return null;

    //Only one "-"
    if (input.ToList().Aggregate(0, (acc, x) =>
        x == '-' ? acc + 1 : acc + 0) > 1) return null;

    //"-" is first
    if (input.ToList().Aggregate(0, (acc, x) =>
        x == '-' ? acc + 1 : acc + 0) == 1 && input[0] != '-') return null;
    return input;
}
[FunctionName("HttpTrigger")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("------------------NEW REQUEST-----------------");
    log.LogInformation("C# HTTP trigger function processed a request.");

    string a = req.Query["a"];
    string b = req.Query["b"];

    string response = ValidateInput(a) != null && ValidateInput(b) != null ?
        responseCalculation(a, b) : null;

    //If no value is null, calculation is returned with status code 200.
    if (response != null) return new OkObjectResult(response);

    //else response is the default message with status code 400.
    return new BadRequestObjectResult(DefaultResponse);
}

Testing

A lot of lines are to validate the input to make it really hard to input malicious code and also two testing steps are made, first just the code functions and then end to end test with curl.

StartUp() => Assert.True(true);
InputDots() => Assert.Null(HttpTrigger.ValidateInput("1.."));
InputDots3() => Assert.Null(HttpTrigger.ValidateInput("..."));
InputDotLast() => Assert.Equal("1.", HttpTrigger.ValidateInput("1."));
InputBraces2() => Assert.Null(HttpTrigger.ValidateInput("["));
InputBraces3() => Assert.Null(HttpTrigger.ValidateInput("("));
InputQoute() => Assert.Null(HttpTrigger.ValidateInput("\""));
InputSingleQoute() => Assert.Null(HttpTrigger.ValidateInput("'"));
InputAlpha() => Assert.Null(HttpTrigger.ValidateInput("abcd"));
InputAlpha2() => Assert.Null(HttpTrigger.ValidateInput("你好,世界"));
InputRocket() => Assert.Null(HttpTrigger.ValidateInput("🚀"));
InputDot() => Assert.Null(HttpTrigger.ValidateInput("."));
InputMiddleMinus() => Assert.Null(HttpTrigger.ValidateInput("1-1"));
IsOne() => Assert.Equal("1", HttpTrigger.ValidateInput("1"));
IsOnePointOne() => Assert.Equal("1.0", HttpTrigger.ValidateInput("1.0"));
IsOnePoint__() => Assert.Equal("1.0000", HttpTrigger.ValidateInput("1.0000"));

Test endpoint with Bash

The bash command cUrl or curl is a preinstalled unix command line tool that can send http-requests. I wrote a Bash-Script that I used on the localhost, but also in production. One example from the script below and full script here.

    # concatinates url with querystring
    url="$hostUrl"a=1\&b=2

    # Sends a get request as url?a=1&b=2
    response=$(curl -s "$url")

    # Checks if response is 3
    if [[ $response == 3 ]]; then
        Points=$((Points + 1)) # True => adds points
    else
        echo error # False => prints error to the console
    fi

Host the function locally with bash

For this we need Azure Functions Core Toolkit

#!/usr/bin/env bash
cd ./Azure_DotnetCore
func start --csharp

Deploy

I used VS Code extension tools to create and start my function. VS Code Azure Function image\

Start and stop with Azure CLI

az login -u <username> -p <password>
az functionapp start --name <function-name> --resource-group <group-name> # or stop

Setting up GitHub workflow for deployment and test

First Azure helps us generate a template (scroll down to preview file button).
template

YAML workflow

Below is the generated template from Azure, but I had to do so some adjustments. I wanted my test project to run and also my endpoint tests.

# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy dotnet core project to Azure Function App - FunctionCalculator

on:
  push:
    branches:
      - master
  workflow_dispatch:

env:
  AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
  DOTNET_VERSION: '3.1.301'  # set this to the dotnet version to use

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - name: 'Checkout GitHub Action'
      uses: actions/checkout@v2

#>>> Added login
    - name: 'Login via Azure CLI'
      uses: azure/login@v1
      with:
        creds: ${ { secrets.AZURE_RBAC_CREDENTIALS } }
#<<<

    - name: Setup DotNet ${ { env.DOTNET_VERSION } } Environment
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: ${ { env.DOTNET_VERSION } }

#>>> Added test build
    - name: 'Run source unit tests'
      shell: bash
      run: |
        pushd './${ { env.AZURE_FUNCTIONAPP_PACKAGE_PATH } }'
        dotnet test
        popd
#<<<
    
    - name: 'Resolve Project Dependencies Using Dotnet'
      shell: bash
      run: |
        pushd './${ { env.AZURE_FUNCTIONAPP_PACKAGE_PATH } }'
        dotnet build --configuration Release --output ./output
        popd

    - name: 'Run Azure Functions Action'
      uses: Azure/functions-action@v1
      id: fa
      with:
        app-name: 'FunctionCalculator'
        slot-name: 'production'
        package: '${ { env.AZURE_FUNCTIONAPP_PACKAGE_PATH } }/output'
        publish-profile: ${ { secrets.AzureAppService_PublishProfile_fee41ad0921045b1a67961f689821848 } }

#>>> Added endpoint tests
    - name: 'Test endpoint with curl'
      shell: bash
      run: |
        pushd './${ { env.AZURE_FUNCTIONAPP_PACKAGE_PATH } }'
        bash testRunner.sh ${ { secrets.AZURE_FUNCTION_URL } }
        popd
#<<<

error/
The given template did not work out of the box so I had to remove the configuration in Azure. I also needed to add the login workflow and a json-formatted secret from azure that was generated by CLI.

az ad sp create-for-rbac --name "FunctionCalculator" --role contributor \
    --scopes /subscriptions/<subscription-id>/resourceGroups/<resource-group> \
    --sdk-auth

It should look like below and is added to repo secrets.AZURE_RBAC_CREDENTIALS.

{"clientId": "<GUID>",
  "clientSecret": "<GUID>",
  "subscriptionId": "<GUID>",
  "tenantId": "<GUID>",
  (...)}

Final Git Pipeline

final pipeline
All 16 tests passed, great!

Security

Serverless apps and functions has the same vulnerabilities as other code connected to the internet. I think that the validation of input data and being extra careful with credentials is whats applicable to this assignment.

References

https://geekflare.com/know-about-serverless/
https://en.wikipedia.org/wiki/Function_as_a_service
https://github.com/RobinAxelsson/AzureFunctionsDotnetCoreCalculator