Serverless email sender using OpenFaaS and .NET Core

Xplicity

Xplicity

SHARE THIS:

Since popularity of serverless programming increasingly growing, I decided to look into it more detailed. From the first glance, we can see two main platforms — Amazon AWS Lambda and Azure Functions. These solutions have a whole infrastructure around them starting with monitoring tools, API gateway service, serverless databases to triggers from message queues, databases or even file storage. However, these solutions are not perfect because our functions are more likely to be closely coupled to selected platform technology stack. Because of this problem I decided to look for other solutions and sharing one of them with you.

While looking for alternative I found OpenFaaS, which is a serverless platform made using Docker infrastructure. It allows deploying functions to Kubernetes or Docker Swarm clusters. Furthermore, it’s Docker-based technology we can run technology inside a container and we will be able to integrate it into OpenFaaS system. So, for testing purposes I decided to create a simple email sender application, that would be able to scale on demand by leveraging the OpenFaaS platform.

PREPARING THE ENVIRONMENT

  1. Install Docker
  2. Initialize Docker Swarm’s master node. (Run command docker swarm init).
  3. Install the OpenFaaS CLI
  4. Download OpenFaaS function templates. (Run command faas-cli template pull https://github.com/openfaas/templates).
  5. Clone OpenFaaS repository and deploy the stack (run deploy_stack.sh file from the cloned repository)
  6. Deploy_stack.sh script will generate admin credentials, that we will use in OpenFaaS CLI and UI.
  7. Login to CLI using faas-cli login –username “generated password” –password “generated password”
  8. Visit OpenFaaS UI to check if everything works as expected.

Currently, there are no deployed functions, but we will add one in a moment.

CREATING A NEW OPENFAAS FUNCTION

So at first, we will create a new function using OpenFaaS CLI command faas-cli new EmailSender –lang csharp. This command will create a EmailSender.yml file and a new directory named EmailSender that contains three generated files: Function.csproj, FunctionHandler.cs and gitignore.

All the functions that need to be built and deployed to OpenFaaS cluster must be declared in EmailSender.yml file:

provider:
  name: faas
  gateway: http://localhost:8080
functions:
  EmailSender:
    lang: csharp
    handler: ./EmailSender
    image: emailsender

Those who are accustomed to Docker might ask where is the function’s DockerFile, it’s declared in templates and copied to build directory when running a faas-cli build command. Although, if you want you can declare your own DockerFile.

Those who are accustomed to Docker might ask where is the function’s DockerFile, it’s declared in templates and copied to build directory when running a faas-cli build command. Although, if you want you can declare your own DockerFile.

Now we can start building email sender function. Let’s open EmailSender/FunctionHandler.cs file and modify it:

using System;
using System.Net;
using System.Net.Mail;
using Newtonsoft.Json;
namespace Function
{
  public class FunctionHandler
  {
    public string Handle(string input)
    {
      if (string.IsNullOrWhiteSpace(input)) {
        throw new ArgumentNullException(nameof(input));
      }
      var newEmail = JsonConvert.DeserializeObject(input);
      var smtpSettings = GetSmtpSettings();
      string result;
      try
      {
        var from = new MailAddress(smtpSettings.SmtpUsername, newEmail.Author);
        var to = new MailAddress(newEmail.Receiver);
        var newEmailMessage = new MailMessage(from, to) {
          Priority = MailPriority.High,
          Body = newEmail.Content,
          Subject = newEmail.Title
        };
        using (var smtp = new SmtpClient(smtpSettings.SmtpHost, smtpSettings.SmtpPort))
        {
           smtp.Credentials = new NetworkCredential(smtpSettings.SmtpUsername, smtpSettings.SmtpPassword);
           smtp.EnableSsl = true;
           smtp.Send(newEmailMessage);
        }
        result = $"Succesfully send a message, payload:{JsonConvert.SerializeObject(newEmail)}";
        }
      catch (Exception ex)
      {
        result = ex.Message;
      }
      return result;
    }
    private static SmtpSettings GetSmtpSettings()
    {
      int GetSmtpPort(string port) => port == null ? 587 : Convert.ToInt32(port);
      var smtpHost = Environment.GetEnvironmentVariable("SmtpHost");
      var smtpPassword = Environment.GetEnvironmentVariable("SmtpPassword");
      var smtpPort = GetSmtpPort(Environment.GetEnvironmentVariable("SmtpPort"));
      var smtpUsername = Environment.GetEnvironmentVariable("SmtpUsername");
      return new SmtpSettings {
        SmtpHost = smtpHost,
        SmtpPassword = smtpPassword,
        SmtpPort = smtpPort,
        SmtpUsername = smtpUsername
      };
    }
  }
}

For making this code work we will create two additional files. NewEmail.cs file contains NewEmail class that declares properties which we need to pass to our function.

namespace Function
{
  public class NewEmail
  {
    public string Author { get; set; }
    public string Receiver { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
  }
}

Furthermore, we need to create SmtpSettings.cs file with SmtpSettings class which contains properties needed for an SMTP connection.

namespace Function
{
  public class SmtpSettings
  {
    public string SmtpHost { get; set; }
    public int SmtpPort { get; set; }
    public string SmtpUsername { get; set; }
    public string SmtpPassword { get; set; }
  }
}

Finally, we need to modify EmailSender.yml file to provide SMTP settings through environment variables (I know it isn’t safest option, but we can improve it later).

provider:
  name: faas
  gateway: http://localhost:8080
functions:
  EmailSender:
    lang: csharp
    handler: ./EmailSender
    image: emailsender
    environment:
      SmtpHost: smtp.gmail.com
      SmtpPort: 587
      SmtpUsername: your_username@gmail.com
      SmtpPassword: your_password

DEPLOYING FUNCTION

At first we need to build newly created function, for that we can use faas-cli build -f ./emailsender.yml command. After running this command we should see output similar to this:

[0] > Building: EmailSender.
Clearing temporary build folder: ./build/EmailSender/
Preparing ./EmailSender/ ./build/EmailSender/function
Building: emailsender with csharp template. Please wait..
Sending build context to Docker daemon  160.3kB
Step 1/20 : FROM microsoft/dotnet:2.1-sdk as builder
....
Step 20/20 : CMD ["fwatchdog"]
 ---> Running in 34af35ffc2cf
Removing intermediate container 34af35ffc2cf
 ---> b757268b4502
Successfully built b757268b4502
Successfully tagged emailsender:latest

Finally, we can deploy our function to OpenFaaS cluster:

faas-cli -action deploy -f ./emailsender.yml
Deploying: EmailSender.
Removing old function.
Deployed.
URL: http://localhost:8080/function/EmailSender

Now we should be able to see our newly created function in UI.

SENDING EMAILS USING THE CREATED FUNCTION

After the function is successfully deployed to OpenFaaS cluster we should be able to use it through HTTP Post method or via OpenFaaS user interface. Let’s select our function and fill the request body with the following information.

After we hit the invoke button, an email will be sent to the selected recipient. Furthermore, the invocation count will be increased so we can keep track of how many calls were made to this function. In case this function receives a lot of calls it will automatically scale from one up to twenty replicas (using default configuration).

echo SuperSecretPassword | docker secret create smtp-password -
docker secret ls
ID                          NAME
ri2keefoyrar92scrv9mmx0r2   api-key
kfastke0gxtm9dnps40k3nrhn   basic-auth-password
cip9t43z7t2ti8grfucz0v6a9   basic-auth-user
3riiu97rac5ls0jr5778in7dy   smtp-password

Now we that created a docker secret for SMTP password we can see the main reason to extract this information from environment variables to docker secrets is that we cannot read this information, only the metadata about it.

docker secret inspect 3riiu97rac5ls0jr5778in7dy
[
    {
        "ID": "3riiu97rac5ls0jr5778in7dy",
        "Version": {
            "Index": 283
        },
        "CreatedAt": "2018-08-15T10:57:23.6925097Z",
        "UpdatedAt": "2018-08-15T10:57:23.6925097Z",
        "Spec": {
            "Name": "smtp-password",
            "Labels": {}
        }
    }
]

Now we can extend code to read this information from /var/openfaas/secrets location inside docker.

private static SmtpSettings GetSmtpSettings()
{
    int GetSmtpPort(string port) => port == null ? 587 : Convert.ToInt32(port);
var smtpHost = Environment.GetEnvironmentVariable("SmtpHost");
    var smtpPassword = ReadSecret("smtp-password");
    var smtpPort = GetSmtpPort(Environment.GetEnvironmentVariable("SmtpPort"));
    var smtpUsername = Environment.GetEnvironmentVariable("SmtpUsername");
    return new SmtpSettings
    {
        SmtpHost = smtpHost,
        SmtpPassword = smtpPassword,
        SmtpPort = smtpPort,
        SmtpUsername = smtpUsername
    };
}
private static string ReadSecret(string secretName)
{
    return File.ReadAllText( ${SecretsLocation}/{secretName}").Trim();
}

ADDING AUTHORIZATION

Lastly, we can add some basic authorization to our function with docker secrets. Let’s create an api-key secret using these commands:

$APIKey = "09f62255f206eea5ae0481feadc22d3092706b4a"
echo $APIKey | docker secret create api-key -

Now let’s add the additional method to authorize function calls.

private static bool Authorize()
{
    var secret = ReadSecret("api-key");
    var headerAuth = Environment.GetEnvironmentVariable("Http_Authorization");

    bool result;
    if (headerAuth == null || headerAuth != "Bearer " + secret)
    {
        result = false;
    } else {
        result = true;
    }
    return result;
}
bool result;
if (headerAuth == null || headerAuth != "Bearer " + secret) {
        result = false;
    } else {
        result = true;
    }
return result;
}

Let’s try whether our authorization is working.

Let’s try to add our created Bearer token to Authorization request header and make a request through Postman.

Now, we can be sure that only authorized users can access and use this function.

CONSLUSION

So that’s it, we have created a serverless function that is not only able to send emails but also is able to automatically increase/decrease the number of replicas depending on traffic through OpenFaaS platform.

All the code discussed here can be found:
https://github.com/Skisas/EmailSender