После недавнего роста популярности бессерверного программирования я решил изучить его. С первого взгляда мы видим две основные платформы - Amazon AWS Lambda и Azure Functions. Эти решения имеют целую инфраструктуру, начиная с инструментов мониторинга, службы шлюза API, бессерверных баз данных и заканчивая триггерами из очередей сообщений, баз данных или даже хранилища файлов. Однако эти решения не идеальны, потому что наши функции, скорее всего, будут тесно связаны с выбранным стеком технологий платформы.

Из-за этой проблемы я решил поискать альтернативу и нашел OpenFaaS, бессерверную платформу, созданную с использованием инфраструктуры Docker. Он позволяет развертывать функции в кластерах Kubernetes или Docker Swarm. Кроме того, это технология на основе Docker, позволяющая запускать технологию внутри контейнера и интегрировать ее в систему OpenFaaS.

Итак, в целях тестирования я решил создать простое приложение для отправки электронной почты, которое можно было бы масштабировать по запросу, используя платформу OpenFaaS.

Подготовка окружающей среды

  1. Установить Докер
  2. Инициализируйте главный узел Docker Swarm. (выполнить команду docker swarm init)
  3. Установите OpenFaaS CLI
  4. Скачать шаблоны функций OpenFaaS (выполнить команду faas-cli template pull https://github.com/openfaas/templates)
  5. Клонировать репозиторий OpenFaaS и развернуть стек (запустить файл deploy_stack.sh из клонированного репозитория)
  6. Скрипт Deploy_stack.sh сгенерирует учетные данные администратора, которые мы будем использовать в интерфейсе командной строки и пользовательского интерфейса OpenFaaS.
  7. Войдите в интерфейс командной строки, используя faas-cli login --username "generated password" --password "generated password"
  8. Посетите OpenFaaS UI, чтобы проверить, все ли работает должным образом.

В настоящее время нет развернутых функций, но мы добавим одну чуть позже.

Создание новой функции OpenFaaS

Итак, сначала мы создадим новую функцию, используя команду OpenFaaS CLI
faas-cli new EmailSender --lang csharp. Эта команда создаст файл EmailSender.yml и новый каталог с именем EmailSender, содержащий три сгенерированных файла: Function.csproj, FunctionHandler.cs и gitignore.

Все функции, которые необходимо собрать и развернуть в кластере OpenFaaS, должны быть объявлены в EmailSender.yml файле:

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

Те, кто привык к Docker, могут спросить, где находится DockerFile функции, он объявлен в templates и скопирован в каталог build при выполнении команды faas-cli build. Хотя при желании вы можете объявить свой собственный файл DockerFile.

Теперь мы можем приступить к созданию функции отправителя электронной почты. Давайте откроем EmailSender/FunctionHandler.cs файл и изменим его:

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<NewEmail>(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
      };
    }
  }
}

Чтобы этот код заработал, мы создадим два дополнительных файла. NewEmail.cs файл содержит NewEmail класс, который объявляет свойства, которые мы должны передать нашей функции.

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; }
  }
}

Кроме того, нам нужно создать SmtpSettings.cs файл с классом SmtpSettings, который содержит свойства, необходимые для SMTP-соединения.

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; }
  }
}

Наконец, нам нужно изменить EmailSender.yml файл, чтобы обеспечить настройки SMTP через переменные среды (я знаю, что это не самый безопасный вариант, но мы можем улучшить его позже).

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

Функция развертывания

Сначала нам нужно создать вновь созданную функцию, для этого мы можем использовать команду
faas-cli build -f ./emailsender.yml. После выполнения этой команды мы должны увидеть следующий результат:

[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

Наконец, мы можем развернуть нашу функцию в кластере OpenFaaS:

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

Теперь мы должны увидеть нашу недавно созданную функцию в пользовательском интерфейсе.

Отправка писем с использованием созданной функции

После успешного развертывания функции в кластере OpenFaaS мы сможем использовать ее через метод HTTP Post или через пользовательский интерфейс OpenFaaS. Выберем нашу функцию и заполним тело запроса следующей информацией.

После того, как мы нажмем кнопку вызова, выбранному получателю будет отправлено электронное письмо. Кроме того, количество вызовов будет увеличено, чтобы мы могли отслеживать, сколько вызовов было выполнено для этой функции. Если эта функция получает много вызовов, она автоматически масштабируется от одной до двадцати реплик (с использованием конфигурации по умолчанию).

Давайте сделаем эту функцию более безопасной

Поскольку мы заставили эту функцию работать, теперь мы можем сделать ее более безопасной, извлекая пароль SMTP из переменных среды в секрет докера.

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       

Теперь, когда мы создали секрет докера для пароля SMTP, мы видим, что основная причина извлечения этой информации из переменных среды в секреты докера заключается в том, что мы не можем прочитать эту информацию, а только ее метаданные.

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": {}
        }
    }
]

Теперь мы можем расширить код для чтения этой информации из места / var / openfaas / secrets внутри докера.

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();
}

Добавление авторизации

Наконец, мы можем добавить базовую авторизацию к нашей функции с помощью секретов докеров. Давайте создадим секрет API-ключа с помощью следующих команд:

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

Теперь давайте добавим дополнительный метод для авторизации вызовов функций.

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;
}

Посмотрим, работает ли наша авторизация.

Попробуем добавить наш созданный токен Bearer в заголовок запроса авторизации и сделать запрос через Postman.

Теперь мы можем быть уверены, что только авторизованные пользователи могут получить доступ и использовать эту функцию.

Заключительные слова

Итак, мы создали бессерверную функцию, которая не только может отправлять электронные письма, но также может автоматически увеличивать / уменьшать количество реплик в зависимости от трафика через платформу OpenFaaS.

Весь обсуждаемый здесь код можно найти:
https://github.com/Skisas/EmailSender

Наконец, позвольте помочь OpenFaaS, разместив его в Github.

Спасибо!