После недавнего роста популярности бессерверного программирования я решил изучить его. С первого взгляда мы видим две основные платформы - Amazon AWS Lambda и Azure Functions. Эти решения имеют целую инфраструктуру, начиная с инструментов мониторинга, службы шлюза API, бессерверных баз данных и заканчивая триггерами из очередей сообщений, баз данных или даже хранилища файлов. Однако эти решения не идеальны, потому что наши функции, скорее всего, будут тесно связаны с выбранным стеком технологий платформы.
Из-за этой проблемы я решил поискать альтернативу и нашел OpenFaaS, бессерверную платформу, созданную с использованием инфраструктуры Docker. Он позволяет развертывать функции в кластерах Kubernetes или Docker Swarm. Кроме того, это технология на основе Docker, позволяющая запускать технологию внутри контейнера и интегрировать ее в систему OpenFaaS.
Итак, в целях тестирования я решил создать простое приложение для отправки электронной почты, которое можно было бы масштабировать по запросу, используя платформу OpenFaaS.
Подготовка окружающей среды
- Установить Докер
- Инициализируйте главный узел Docker Swarm. (выполнить команду
docker swarm init
) - Установите OpenFaaS CLI
- Скачать шаблоны функций OpenFaaS (выполнить команду
faas-cli template pull https://github.com/openfaas/templates
) - Клонировать репозиторий OpenFaaS и развернуть стек (запустить файл deploy_stack.sh из клонированного репозитория)
- Скрипт Deploy_stack.sh сгенерирует учетные данные администратора, которые мы будем использовать в интерфейсе командной строки и пользовательского интерфейса OpenFaaS.
- Войдите в интерфейс командной строки, используя
faas-cli login --username "generated password" --password "generated password"
- Посетите 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.
Спасибо!