Postbox
Введение
В этом примере мы не будем повторять инструкции по настройке адреса электронной почты в Yandex Cloud Postbox, которые вы можете найти в документации. Предполагается, что вы уже настроили адрес электронной почты и готовы отправлять сообщения через Yandex Cloud Postbox.
SDK
Для работы с Yandex Cloud Postbox в Go мы будем использовать библиотеку aws-sdk-go-v2
.
В этом примере мы используем EndpointResolverV2
— новый протокол разрешения эндпоинтов. Он позволяет
задавать правила разрешения эндпоинтов для различных сервисов на уровне сервиса, не глобально для всего SDK.
Подробнее в документации AWS SDK.
Код
Так как Yandex Cloud Postbox является совместимым с AWS сервисом, то для отправки сообщений мы можем использовать совместимый с AWS формат ключей — в Яндекс Облаке это статические ключи доступа, которые можно создать в IAM.
Но из-за того, что Yandex Cloud Postbox также поддерживает авторизацию при помощи IAM-токенов, я бы хотел показать и такой пример. Поэтому ниже будет рассказано про 2 варианта serverless функций с разными методами авторизации.
Константы
Для начала давайте заведем константы, которые будем использовать в примерах.
package main
const (
// Sender address must be verified with Amazon SES.
Sender = "noreply@yourdomain.com"
// Recipient address.
Recipient = "receiver@domain.com"
// Subject line for the email.
Subject = "Yandex Cloud Postbox Test via AWS SDK for Go"
// HtmlBody is the body for the email.
HtmlBody = "<h1>Amazon SES Test Email (AWS SDK for Go)</h1><p>This email was sent with " +
"<a href='https://yandex.cloud/ru/docs/postbox/quickstart'>Yandex Cloud Postbox</a> using the " +
"<a href='https://aws.amazon.com/sdk-for-go/'>AWS SDK for Go</a>.</p>"
// TextBody is the email body for recipients with non-HTML email clients.
TextBody = "This email was sent with Yandex Cloud Postbox using the AWS SDK for Go."
// CharSet The character encoding for the email.
CharSet = "UTF-8"
)
На 5
строке мы указываем email адрес, с которого будут отправляться сообщения. Он должен быть именно в том домене,
который вы настроили в Yandex Cloud Postbox.
На 8
указываем email адрес, на который будут отправляться сообщения. Он может быть любым.
Резолвер
Следующим шагом создадим резолвер для разрешения кастомного эндпоинта Postbox.
package main
import (
"context"
"net/url"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
"github.com/aws/smithy-go/endpoints"
)
type resolverV2 struct{}
func (*resolverV2) ResolveEndpoint(ctx context.Context, params sesv2.EndpointParameters) (
transport.Endpoint, error,
) {
u, err := url.Parse("https://postbox.cloud.yandex.net")
if err != nil {
return transport.Endpoint{}, err
}
return transport.Endpoint{
URI: *u,
}, nil
}
Как видно, тут ничего сложного. Мы просто создаем тип имплементирующий интерфейс
sesv2.EndpointResolverV2
.
AWS совместимый вариант
package main
import (
"context"
"encoding/json"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
)
func AwsHandler(ctx context.Context) ([]byte, error) {
// Create an SES session.
client := sesv2.New(sesv2.Options{
Region: "ru-central1",
EndpointResolverV2: &resolverV2{},
// The following options are useful for debugging
//ClientLogMode: aws.LogRequestWithBody | aws.LogResponseWithBody,
//Logger: logging.NewStandardLogger(
// os.Stdout,
//),
})
res, err := sendEmail(ctx, client)
if err != nil {
return nil, err
}
// return response
resp := map[string]interface{}{
"result": "success",
"messageId": res.MessageId,
}
respBytes, err := json.Marshal(resp)
if err != nil {
panic(err)
}
return respBytes, nil
}
Как видно нам не пришлось ничего менять в коде, чтобы использовать Postbox вместо SES. Мы просто создали новый клиент
для работы с Postbox, передав ему EndpointResolverV2
для разрешения кастомного эндпоинта Postbox.
Вы можете заметить, что мы не передавали никаких ключей доступа в клиент, так как при создании этой функции, мы укажем их в переменных окружения. Клиент сам возьмет их оттуда.
Собственно отправка сообщения происходит на строке в SendEmail
, который вынесен в отдельный файл, так как
переиспользуется в обоих примерах.
package main
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
"github.com/aws/aws-sdk-go-v2/service/sesv2/types"
)
func sendEmail(ctx context.Context, client *sesv2.Client) (*sesv2.SendEmailOutput, error) {
// Assemble the email.
input := &sesv2.SendEmailInput{
Destination: &types.Destination{
ToAddresses: []string{Recipient},
},
Content: &types.EmailContent{
Simple: &types.Message{
Subject: &types.Content{
Charset: aws.String(CharSet),
Data: aws.String(Subject),
},
Body: &types.Body{
Html: &types.Content{
Charset: aws.String(CharSet),
Data: aws.String(HtmlBody),
},
Text: &types.Content{
Charset: aws.String(CharSet),
Data: aws.String(TextBody),
},
},
},
},
FromEmailAddress: aws.String(Sender),
}
// Attempt to send the email.
res, err := client.SendEmail(ctx, input)
return res, err
}
В остальном никаких больше особенностей по работе с Postbox в Yandex Cloud нет, и вы можете использовать
стандартные методы работы с SESv2 из aws-sdk-go-v2
. Список поддерживаемых методов можно найти в
документации.
Вариант с авторизацией через IAM-токен
Для этого нам понадобится реализовать middleware для авторизации через IAM-токен. Для этого нам понадобится реализовать
структуру iamRequestMiddleware
и метод HandleFinalze
для реализации интерфейса
FinalizeMiddleware
.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
"github.com/aws/smithy-go/middleware"
"github.com/aws/smithy-go/tracing"
smithyhttp "github.com/aws/smithy-go/transport/http"
)
type iamToken struct {
Token string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type iamRequestMiddleware struct {
options sesv2.Options
}
func (*iamRequestMiddleware) ID() string {
return "IamToken"
}
func (m *iamRequestMiddleware) HandleFinalize(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
) {
_, span := tracing.StartSpan(ctx, "IamToken")
defer span.End()
req, ok := in.Request.(*smithyhttp.Request)
if !ok {
return out, metadata, fmt.Errorf("unexpected transport type %T", in.Request)
}
tokenValue := ctx.Value("lambdaRuntimeTokenJSON")
tokenStr, ok := tokenValue.(string)
if !ok {
return out, metadata, fmt.Errorf("unexpected token type %T", tokenValue)
}
var token iamToken
err = json.Unmarshal([]byte(tokenStr), &token)
if err != nil {
return middleware.FinalizeOutput{}, middleware.Metadata{}, err
}
req.Header.Set("X-YaCloud-SubjectToken", token.Token)
span.End()
return next.HandleFinalize(ctx, in)
}
func swapAuth() func(options *sesv2.Options) {
return func(options *sesv2.Options) {
options.APIOptions = append(options.APIOptions, func(stack *middleware.Stack) error {
_, err := stack.Finalize.Swap("Signing", &iamRequestMiddleware{})
if err != nil {
return err
}
return nil
})
}
}
Отдельно добавим метод swapAuth
. Он реализует паттерн функциональных опций. В нем мы получаем возможность подменить
middleware для авторизации, которая выполняет подпись запроса. Вместо нее мы будем использовать наш middleware,
который извлекает IAM-токен из контекста и кладет его в заголовок X-YaCloud-SubjectToken
запроса.
package main
import (
"context"
"encoding/json"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
)
func YcHandler(ctx context.Context) ([]byte, error) {
// Create an SES session.
client := sesv2.New(
sesv2.Options{
Region: "ru-central1",
EndpointResolverV2: &resolverV2{},
},
swapAuth(),
)
res, err := sendEmail(ctx, client)
if err != nil {
return nil, err
}
// return response
resp := map[string]interface{}{
"result": "success",
"messageId": res.MessageId,
}
respBytes, err := json.Marshal(resp)
if err != nil {
panic(err)
}
return respBytes, nil
}
Собственно вариант функции с авторизацией через IAM-токен отличается только наличием 17
строки, где мы добавляем
middleware для подмены метода авторизации.
Развертывание
Для развертывания этой функции в Yandex Cloud вы можете воспользоваться Terraform описанным в примере.
data "archive_file" "function_files" {
output_path = "./function.zip"
source_dir = "../function"
type = "zip"
}
resource "yandex_function" "postbox_aws" {
name = "postbox-aws"
user_hash = data.archive_file.function_files.output_sha256
runtime = "golang121"
entrypoint = "index.AwsHandler"
memory = "128"
execution_timeout = "10"
content {
zip_filename = data.archive_file.function_files.output_path
}
service_account_id = yandex_iam_service_account.postbox_sender.id
environment = {
AWS_SECRET_ACCESS_KEY = yandex_iam_service_account_static_access_key.postbox_sender_key.secret_key
AWS_ACCESS_KEY_ID = yandex_iam_service_account_static_access_key.postbox_sender_key.access_key
}
}
resource "yandex_function" "postbox_yc" {
name = "postbox-yc"
user_hash = data.archive_file.function_files.output_sha256
runtime = "golang121"
entrypoint = "index.YcHandler"
memory = "128"
execution_timeout = "10"
content {
zip_filename = data.archive_file.function_files.output_path
}
service_account_id = yandex_iam_service_account.postbox_sender.id
}
locals {
functions = {
aws = yandex_function.postbox_aws,
yc = yandex_function.postbox_yc,
}
}
// IAM binding for making function public
resource "yandex_function_iam_binding" "postbox_function_binding" {
for_each = local.functions
function_id = each.value.id
role = "functions.functionInvoker"
members = ["system:allUsers"]
}
Для этого мы сначала определяем ресурс archive_file
для создания zip-архива с функцией.
data "archive_file" "function_files" {
output_path = "./function.zip"
source_dir = "../function"
type = "zip"
}
А затем ресурс yandex_function
для создания функции в Yandex Cloud, куда передаем zip-архив с функцией.
resource "yandex_function" "postbox_aws" {
name = "postbox-aws"
user_hash = data.archive_file.function_files.output_sha256
runtime = "golang121"
entrypoint = "index.AwsHandler"
memory = "128"
execution_timeout = "10"
content {
zip_filename = data.archive_file.function_files.output_path
}
service_account_id = yandex_iam_service_account.postbox_sender.id
environment = {
AWS_SECRET_ACCESS_KEY = yandex_iam_service_account_static_access_key.postbox_sender_key.secret_key
AWS_ACCESS_KEY_ID = yandex_iam_service_account_static_access_key.postbox_sender_key.access_key
}
}
Дополнительно, мы передаем переменные окружения AWS_ACCESS_KEY_ID
и AWS_SECRET_ACCESS_KEY
для авторизации в
Yandex Cloud Postbox.
Аналогично, мы создаем вторую функцию, которая будет использовать авторизацию по IAM-токену. В нее вы не передаем ключи, а привязываем к ней сервисный аккаунт, который имеет доступ к Postbox.
resource "yandex_function" "postbox_yc" {
name = "postbox-yc"
user_hash = data.archive_file.function_files.output_sha256
runtime = "golang121"
entrypoint = "index.YcHandler"
memory = "128"
execution_timeout = "10"
content {
zip_filename = data.archive_file.function_files.output_path
}
service_account_id = yandex_iam_service_account.postbox_sender.id
}
Чтобы сделать функции доступными из интернета, мы можем создать ресурс yandex_function_iam_binding
что бы разрешить доступ к функциям всем пользователям, тем самым сделав их публичными.
locals {
functions = {
aws = yandex_function.postbox_aws,
yc = yandex_function.postbox_yc,
}
}
// IAM binding for making function public
resource "yandex_function_iam_binding" "postbox_function_binding" {
for_each = local.functions
function_id = each.value.id
role = "functions.functionInvoker"
members = ["system:allUsers"]
}
Запуск
Теперь, когда функции развернуты, вы можете отправить сообщение вызвав функцию:
SEND_FUNC_ID=$(terraform -chdir=./tf output -raw aws_function_id)
curl -XPOST \
"https://functions.yandexcloud.net/$SEND_FUNC_ID" \
-H "Content-Type: application/json"
Или, чтобы вызвать вторую функцию, которая использует IAM-токен:
SEND_FUNC_ID=$(terraform -chdir=./tf output -raw yc_function_id)
curl -XPOST \
"https://functions.yandexcloud.net/$SEND_FUNC_ID" \
-H "Content-Type: application/json"
После этого вы получите сообщение на указанный email адрес.