All articles
Microservices e-mail Cloud : guide d'utilisation d'AWS Lambda et Cloudflare Workers

Microservices e-mail Cloud : guide d'utilisation d'AWS Lambda et Cloudflare Workers

Déployez un microservice e-mail sur Lambda et gérez les files de messages, invocable depuis Cloudflare Workers ou n'importe quel backend Node.js.

Résumer avec l'IA :

AM
Antonin Marxer
· 15 min read0

Tu as développé ta page de contact et tu veux envoyer un e-mail à la soumission du formulaire ?

Dans cet article, on va voir comment envoyer des e-mails gratuitement avec :

  • Une API e-mail tierce, avec un comparatif des solutions existantes.

  • AWS Lambda pour héberger le microservice e-mail.

  • Le backend de ton app en Node.js ou avec le méta-framework remix.run, hébergé sur Cloudflare Workers dans une architecture en microservices.

Tu vas apprendre à :

  • Créer un microservice e-mail avec AWS Lambda et la file AWS SQS.

  • Créer un template CloudFormation et un template SAM.

  • Déployer sur AWS de façon reproductible.

  • Publier un événement vers AWS avec @aws-sdk/client-sqs depuis ton app Node.js/Remix.

  • Créer un microservice dans Cloudflare avec Cloudflare Workers et wrangler.
    Ce worker Cloudflare centralise la logique de publication d’événements, pour que les autres workers se contentent de l’appeler pour déclencher notre logique d’envoi d’e-mails AWS.

Tu peux retrouver tout le code sur mon repo GitHub.

Pour des raisons de compatibilité, j’utilise des bibliothèques qui fonctionnent sur Cloudflare Workers.
Les workers Cloudflare ne tournent pas sur Node.js mais sur des isolates V8, le même moteur de navigateur que celui de Chrome et Chromium.

Créer un FaaS e-mail avec AWS Lambda

On peut partir sur n’importe quelle solution d’envoi :

Service d’API e-mail Offre gratuite / essai Limite du plan gratuit Notes sur le plan gratuit Tarif après l’offre gratuite (exemple) Débit maximal du plan gratuit
Resend Oui 3 000 e-mails/mois, 100 e-mails/jour Plan gratuit généreux : 1 domaine personnalisé, rétention des données 1 jour, support par ticket. Le surplus est bloqué une fois le quota mensuel atteint. Pro : 20 $/mois pour 50 000 e-mails ; Scale : 90 $/mois pour 100 000 e-mails ; option IP dédiée disponible 2 e-mails par seconde
SendGrid Oui 100 e-mails/jour pendant 60 jours Bon pour les petits volumes et les tests Formules payantes à partir de 19,95 $/mois pour 50 000 e-mails ~600 requêtes API/min sur la plupart des endpoints
Mailgun Oui 100 e-mails/jour Essai le premier mois À partir de 35 $/mois pour plus de 10 000 e-mails ~300 messages/min
Postmark Pas de plan gratuit, paiement à l’usage Pas de plan gratuit Axé sur la délivrabilité 15 $ pour 10 000 e-mails
Amazon SES Oui pendant les 12 premiers mois 3 000 e-mails/mois Intégration à l’écosystème AWS 0,10 $ pour 1 000 e-mails après l’offre gratuite 14 e-mails/s, avec des rafales jusqu’à 28 e-mails/s pour les nouveaux comptes, ajustable sur demande
MailerSend Oui 12 000 e-mails/mois Plan gratuit généreux pour les petites équipes. Le surplus coûte 1,00 $ / 1 000 e-mails Formules à partir de 15 $/mois
Brevo (Sendinblue) Oui 300 e-mails/jour Limite d’envoi quotidienne Formules payantes à partir de 15 $/mois
Elastic Email Oui 100 e-mails/jour Petit plan gratuit quotidien Formules payantes à partir d’environ 9 $/mois
Mailtrap Oui, surtout pour les tests 3 500 e-mails/mois Pensé pour la préproduction et les tests Formules payantes à partir de 10 $/mois

Côté tarifs, Brevo, Mailtrap, Amazon SES et Resend semblent de bons candidats.
Je pars sur Resend pour son tableau de bord, qui permet d’exporter en CSV.

Toutes les API e-mail imposent un débit maximal. Avec Resend, au-delà de 2 e-mails par seconde tu reçois une erreur de rate limit (429). Plutôt qu’un appel direct à l’API e-mail, il vaut donc mieux passer par une file.

Consommer les e-mails (Pub/Sub)
Dans un système de file, on a des publishers et des subscribers. Le publisher crée la tâche à traiter. Le subscriber écoute la file. Quand une tâche arrive, on dit qu’elle est consommée.

Dans notre cas, quand un e-mail doit partir, on crée une tâche dans la file. Le handler de la tâche est abonné à la file et envoie l’e-mail via l’API Resend.

Profiter des files des plateformes cloud
Pour rester simple, on utilise le FaaS (Functions as a Service) des plateformes cloud avec la file qu’elles proposent : ça ne coûte rien et consomme moins de ressources.

On peut monter notre microservice e-mail avec :

  • Cloudflare, avec un worker Cloudflare et des queues Cloudflare.

  • AWS : une fonction Lambda avec Amazon Simple Queue Service (SQS).

Les queues Cloudflare coûtent 5 $/mois, hors budget pour ce challenge.
→ On part sur AWS.

Implémenter le microservice e-mail avec AWS Lambda

L’e-mail à envoyer doit être validé. On utilise yup (on verra pourquoi plus loin).

src/schema.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export const schema = yup
.object({
to: recipientsSchema, // single or multiple recipients
from: senderSchema,
subject: yup.string().required(),
html: yup.string(),
text: yup.string(),
})
.test(
"html-or-text-required",
"Either html or text must be provided",
function (value) {
return !!(value?.html || value?.text);
},
);
// single recipient
const recipientSchema = yup.object({
name: yup.string().nullable().optional(),
email: yup.string().email().required(),
});
// multiple recipients
const recipientsSchema = yup.lazy((value) => {
if (Array.isArray(value)) {
return yup.array().of(recipientSchema).required();
}
return recipientSchema.required();
});
const senderSchema = yup
.object({
name: yup.string().nullable().optional(),
email: yup.string().email().required(),
})
//
.transform((value, originalValue) => {
if (
originalValue === undefined ||
originalValue === null ||
(typeof originalValue === "object" &&
originalValue !== null &&
Object.keys(originalValue).length === 0)
) {
return undefined;
}
return value;
})
.optional();

L’entrée accepte soit un destinataire, soit un tableau de destinataires.
Chaque destinataire a un nom optionnel et un e-mail obligatoire.
L’expéditeur est optionnel, tout comme son nom.

Envoyer un e-mail avec Resend

Pour appeler l’API Resend, on utilise leur bibliothèque npm resend, en respectant ces conditions :

  • En cas d’erreur de rate limit, réessayer après la durée de throttling.

  • Les champs to et from doivent prendre la forme ${recipient.name} <${recipient.email}>, ou tout simplement recipient.name.

src/send/resend.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Schema } from "../schema";
import { CreateEmailOptions, Resend } from "resend";
import { getRecipientsField, getSenderField } from "../helpers/email";
// Resend has a limit of 2 emails per second
const retryAfter = 500;
export async function sendEmail(body: Schema, resend?: Resend) {
if (!resend) {
throw new Error("Resend instance is required");
}
const payload: CreateEmailOptions = {
from: getSenderField(body.from),
to: getRecipientsField(body.to),
subject: body.subject,
html: body.html,
text: body.text,
react: undefined,
};
const { data, error } = await resend.emails.send(payload);
if (error) {
if (isResendRateLimitError(error)) {
await sleep(retryAfter);
return sendEmail(body, resend);
}
// Throw to trigger Lambda retry and DLQ if needed
throw new Error(JSON.stringify({ ...error, payload }));
}
}
function isResendRateLimitError(error: unknown): boolean {
if (typeof error !== "object" || error === null) return false;
// Resend SDK returns the error as a plain Error with a message string
const message = (error as Error).message;
return message?.includes("rate_limit_exceeded");
}
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
helpers/email.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { RecipientSchema, RecipientsSchema, SenderSchema } from "../schema";
export const getSenderField = (sender?: SenderSchema) => {
if (!sender) {
return `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`;
}
if (sender.name?.trim()) {
return `${sender.name} <${sender.email}>`;
}
return sender.email;
};
export const getRecipientsField = (
recipient: RecipientsSchema,
): string | string[] => {
if (Array.isArray(recipient)) {
return recipient.map((r) => getRecipientField(r));
}
return getRecipientField(recipient);
};
const getRecipientField = (recipient: RecipientSchema) => {
if (recipient.name?.trim()) {
return `${recipient.name} <${recipient.email}>`;
}
return recipient.email;
};

S’authentifier auprès d’une API tierce depuis AWS Lambda

L’appel à l’API e-mail de Resend doit être authentifié. Sa clé API doit être stockée de façon sécurisée. Pour ça, on utilise AWS Secrets Manager (SSM).

  1. Créer le secret dans SSM.

  2. Habituellement, pour récupérer le secret, on lie des variables par leur clé dans la configuration de la fonction.
    Mais ça le rend visible dans la configuration d’environnement de la fonction.
    Alternative : récupérer le secret SSM à l’exécution avec @aws-sdk/client-secrets-manager.

src/index.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
async function getResendApiKey(): Promise<string> {
const client = new SecretsManagerClient({});
const command = new GetSecretValueCommand({
SecretId: process.env.RESEND_SECRET_NAME,
});
const response = await client.send(command);
if (!response.SecretString) {
throw new Error("Resend API key not found");
}
return response.SecretString;
}

Voyons maintenant la logique principale : comment notre fonction Lambda gère-t-elle les messages Amazon SQS ?

Implémenter un handler de messages AWS SQS

Pour gérer les messages des files SQS, on exporte un handler de type SQSHandler :

index.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const allowedSenderEmails = (process.env.ALLOWED_SENDERS
|| "contact@example.com").split(",");
export const handler: SQSHandler = async (event: SQSEvent) => {
const resend = new Resend(await getResendApiKey());
const failedRecords: SQSBatchResponse["batchItemFailures"] = [];
for (const record of event.Records) {
try {
if (!record.body) {
throw new Error("Invalid input");
}
const body = JSON.parse(record.body);
await validateBodyAsync(body);
// Validate sender email if provided
if (body.from_email && !allowedSenderEmails.includes(body.from_email)) {
throw new Error(
`Sender email ${body.from_email} is not allowed. Allowed emails: ${allowedSenderEmails.join(", ")}`,
);
}
await sendResendEmail(body, resend);
} catch (err) {
console.error(err);
failedRecords.push({
itemIdentifier: record.messageId,
});
}
}
if (failedRecords.length > 0) {
return {
batchItemFailures: failedRecords,
};
}
};

La valeur de retour batchItemFailures indique à Lambda quels messages précis du lot ont échoué, pour qu’AWS ne remette en file que ceux-là au lieu de rejouer tout le lot.

Sans batchItemFailures, un seul message en échec fait échouer tout le lot.
Ça peut entraîner :

  • Un traitement en double des messages déjà passés.

  • Des temps de traitement plus longs.

  • Des coûts plus élevés (si tu as pris un plan e-mail premium).

Configurer et invoquer la fonction Lambda avec SAM

La fonction Lambda peut s’invoquer avec Serverless Framework ou AWS SAM (Serverless Application Model). La principale différence : Serverless Framework est multi-cloud, alors que SAM ne gère que AWS (mais de façon profonde). Par simplicité, on prend AWS SAM : tout est intégré.

samconfig.toml
ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1
[default.global.parameters]
stack_name = "prod-notification-service-sh"
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
resolve_s3 = true
s3_prefix = "prod-notification-service-sh"
region = "eu-west-3"
[default.package.parameters]
resolve_s3 = true
s3_prefix = "prod-notification-service-sh"
output_template_file = "packaged.yaml"
[default.sync.parameters]
watch = true
stack_name = "prod-notification-service-sh"
[default.local_start_api.parameters]
warm_containers = "EAGER"
[default.local_start_lambda.parameters]
warm_containers = "EAGER"

Pour invoquer la fonction, il faut simuler un événement SQS, ce qu’on fera dans un cas de test jest.

__test__/test-handler.test.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import type {
SQSEvent,
SQSMessageAttributes,
SQSRecordAttributes,
} from "aws-lambda";
import { handler } from "../src";
import type { Schema } from "../src/schema";
import { v4 as uuidv4 } from "uuid";
describe("Unit test for app handler", function () {
it("sends text email", async () => {
const emails: Schema[] = [
{
to: { email: "YOUR_EMAIL@gmail.com" },
subject: "Test Email",
text: "This is a test email.",
},
];
const events = getSQSEvents(emails);
const result = await handler(events, {} as any, () => {});
// Add assertions here to verify the email was sent correctly
});
});
const getSQSEvents = (emails: Schema[]): SQSEvent => {
const attributes: SQSRecordAttributes = {
ApproximateReceiveCount: "0",
SentTimestamp: "1630456800000",
SenderId: "",
ApproximateFirstReceiveTimestamp: "1630456800000",
};
const messageAttributes: SQSMessageAttributes = {
dummy: {
dataType: "String",
stringValue: "dummy",
},
};
const event: SQSEvent = {
Records: emails.map((email) => ({
messageId: uuidv4(),
receiptHandle: "",
attributes,
messageAttributes,
md5OfBody: "",
md5OfMessageAttributes: "",
eventSource: "",
eventSourceARN: "",
awsRegion: "eu-west-3",
body: JSON.stringify(email),
})),
};
return event;
};

Pour lancer le test, ajoute "test": "jest" à ton package.json, puis lance pnpm run test.

Déployer ta fonction Lambda

Toujours pour rester simple, on utilise CloudFormation pour déployer la fonction Lambda et les files.
Pas de Terraform ici : le code de la fonction Lambda finirait dans le state TF.

Le déploiement suit cette architecture :

Architecture AWS

  1. Frontend : soumet le formulaire de contact et envoie une requête POST anonyme au backend.

  2. Backend : reçoit l’e-mail à envoyer et publie une tâche dans la file SQS (qu’il tourne sur Node.js ou sur un isolate V8).

  3. Amazon SQS : contient les e-mails à envoyer.

  4. AWS Lambda : s’abonne à la file (3) et traite les e-mails en les envoyant via une API e-mail prise en charge (6) ou (7).

  5. Bucket S3 : héberge le code de la fonction Lambda. Il est créé implicitement par CloudFormation.

  6. Dead Letter Queue : contient les messages qui n’ont pas pu être traités après 3 tentatives.

  7. Resend : API e-mail (nécessite de stocker un secret dans SSM).

  8. AMAZON SES : API e-mail (aucun secret requis, intégrée à l’écosystème AWS).

Montons le template.yaml CloudFormation :

yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
A serverless application that processes email notifications from an SQS queue.
Globals:
Function:
Timeout: 30
MemorySize: 256
Tracing: Active
LoggingConfig:
LogFormat: JSON
Environment:
Variables:
EMAIL_SERVICE: "resend"
FROM_NAME: "Your Firm"
FROM_EMAIL: "contact@example.com"
ALLOWED_SENDERS_EMAILS: "contact@example.com"
RESEND_SECRET_NAME: "prod-notification-service-resend-api-key"
Resources:
NotificationServiceLambda:
Type: AWS::Serverless::Function
Properties:
FunctionName: prod-notification-service-sh-lambda
CodeUri: ./
Handler: dist/src/index.handler
Runtime: nodejs20.x
Architectures:
- x86_64
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt SQSQueue.Arn
BatchSize: 2
Enabled: true
Policies:
- SQSPollerPolicy:
QueueName: !GetAtt SQSQueue.QueueName
- Statement:
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
Resource: "*"
- Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:YOUR_REGION:YOUR_ACCOUNT_ID:secret:prod-notification-service-*
SQSQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: prod-notification-service-sh-queue
VisibilityTimeout: 120
RedrivePolicy:
deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn
maxReceiveCount: 3
DeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: prod-notification-service-sh-deadletter-queue
MessageRetentionPeriod: 1209600 # 14 days in seconds
Outputs:
NotificationServiceLambda:
Description: Notification Service Lambda Function ARN
Value: !GetAtt NotificationServiceLambda.Arn
SQSQueueURL:
Description: URL of the SQS Queue
Value: !Ref SQSQueue
SQSQueueARN:
Description: ARN of the SQS Queue
Value: !GetAtt SQSQueue.Arn
DeadLetterQueueURL:
Description: URL of the Dead Letter Queue
Value: !Ref DeadLetterQueue
DeadLetterQueueARN:
Description: ARN of the Dead Letter Queue
Value: !GetAtt DeadLetterQueue.Arn

Déclare ces scripts dans ton package.json :

json
1
2
3
4
5
6
7
"scripts": {
"build": "rm -rf dist && tsc && sam build",
"deploy": "pnpm run build && sam deploy --disable-rollback",
"delete": "aws cloudformation delete-stack --stack-name prod-notification-service-sh --region eu-west-3",
"log": "sam logs -n NotificationServiceLambda --stack-name prod-notification-service-sh --tail",
"test": "jest"
},

Pour déployer d’un coup le template CloudFormation et le code Lambda :

  1. Créer le secret directement dans le tableau de bord Amazon SSM.

    Note : dans le repo GitHub, le secret SSM est créé avec Terraform.

  2. Lancer pnpm run deploy.
    La commande transpile le TypeScript en JavaScript, empaquette le code avec sam build et déploie avec sam deploy.

Ça crée la stack CloudFormation prod-notification-service-sh dans ta région configurée. Tu peux suivre ses ressources dans le service AWS CloudFormation de la console AWS.
Pour tout remettre à zéro, lance : aws cloudformation delete-stack --stack-name prod-notification-service-sh --region YOUR_REGION

Et voilà, tout tourne !

Publier un événement vers AWS SQS

Si on arrive à publier un événement dans la file qu’écoute notre fonction Lambda, l’e-mail part.

On peut publier un événement directement avec @aws-sdk/client-sqs. Mais si 10 applications appellent ta fonction Lambda, ça fait beaucoup de code dupliqué.

Si ton app tourne sur remix.run et Cloudflare Workers, je te recommande de créer un worker Cloudflare dont l’unique rôle est de publier les tâches d’e-mail vers Amazon SQS :

Architecture Cloudflare Workers

  1. Utilisateur : soumet le formulaire de contact à ton app.

  2. Worker de l’app : ton app fait un simple appel HTTP à ton mail-worker.

  3. Worker du microservice e-mail : ton mail-worker gère la publication des événements SQS.

  4. Grafana Loki : surveille les éventuelles erreurs, en les publiant vers Loki.

  5. File SQS : met en file les événements e-mail.

Pour publier les éventuelles erreurs vers Loki et recevoir des alertes par e-mail :

  1. Créer un compte Grafana Cloud : un compte sur Grafana Cloud donne accès à Loki, un système d’agrégation de logs.

  2. Configurer Loki : intégrer Loki à ton application pour collecter les logs. Via l’API Loki ou une bibliothèque de logging compatible, tes logs partent vers Loki.

  3. Créer des alertes dans Grafana : définir des règles d’alerte basées sur les logs collectés par Loki. Tu peux déclencher une alerte sur un message d’erreur précis ou sur un motif de log.

  4. Configurer les notifications e-mail : brancher un canal de notification e-mail dans Grafana et le relier à tes règles d’alerte, pour recevoir un e-mail quand une alerte se déclenche.

Si tu es sur le plan Cloudflare starter, préfère Cloudflare Worker Tailing pour suivre les logs de ton worker.
Loki fonctionne en push (le worker pousse les logs vers Loki avec workers-loki-logger), alors que le tailing fonctionne en pull. Même si le worker Cloudflare plante au démarrage, les logs suivis sont quand même récupérés, ce qui n’est pas le cas avec une méthode en push.

Implémentons le worker-email-microservice

src/index.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { Logger } from "workers-loki-logger";
import {
SQSClient,
SendMessageCommand,
} from "@aws-sdk/client-sqs";
type Bindings = {
AWS_SQS_ACCESS_KEY_ID: string;
AWS_SQS_SECRET_ACCESS_KEY: string;
AWS_SQS_REGION: string;
AWS_SQS_QUEUE_URL: string;
LOKI_SECRET: string;
LOKI_URL: string;
ENVIRONMENT: string;
};
function getLogger(context: ExecutionContext, env: Bindings) {
return new Logger({
cloudflareContext: context,
lokiSecret: env.LOKI_SECRET,
lokiUrl: env.LOKI_URL || "https://logs-prod-eu-west-0.grafana.net",
stream: {
worker: "newsletter-worker",
environment: env.ENVIRONMENT,
},
});
}
async function handleSendEmail(
req: Request,
env: Bindings,
ctx: ExecutionContext,
logger: Logger,
): Promise<Response> {
const sqsClient = new SQSClient({
region: env.AWS_SQS_REGION,
credentials: {
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
},
});
const parsedBody = await req.json();
const command = new SendMessageCommand({
QueueUrl: env.AWS_SQS_QUEUE_URL,
MessageBody: JSON.stringify(parsedBody),
});
console.log(`Sending email to SQS: ${JSON.stringify(command)}`);
const result = await sqsClient.send(command);
return result.$metadata.httpStatusCode === 200
? new Response("Email queued in SQS")
: Response.json({
status: 500,
message: "Failed to queue email in SQS",
requestId: result.$metadata.requestId,
});
}
export default {
async fetch(
req: Request,
env: Bindings,
ctx: ExecutionContext,
): Promise<Response> {
const logger = getLogger(ctx, env);
logger.mdcSet("requestUrl", req.url);
try {
const url = new URL(req.url);
// Send email directly on POST request
if (req.method === "POST" && url.pathname === "/send-email") {
return handleSendEmail(req, env, ctx, logger);
}
logger.error(
"Route not found",
new Error(`Route ${req.method} ${url.pathname} not found`),
);
return new Response("Not found", { status: 404 });
} catch (error) {
logger.error("Caught error", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
} finally {
await logger.flush();
}
}
}

Déclare les secrets nécessaires dans .dev.vars pour le développement local :

.dev.vars
ini
1
2
3
4
5
AWS_SQS_ACCESS_KEY_ID=
AWS_SQS_SECRET_ACCESS_KEY=
AWS_SQS_REGION=eu-west-3
AWS_SQS_QUEUE_URL=
LOKI_SECRET=

Déclare LOKI_URL dans wrangler.toml.

Pour déployer le worker en production :

bash
1
2
3
4
5
6
7
8
9
pnpm install
# Define all secrets on remote worker.
# On the first time, it will also create the worker.
npx wrangler secret put -e production AWS_SQS_ACCESS_KEY_ID
npx wrangler secret put -e production AWS_SQS_SECRET_ACCESS_KEY
npx wrangler secret put -e production AWS_SQS_REGION
npx wrangler secret put -e production AWS_SQS_QUEUE_URL
npx wrangler secret put -e production LOKI_SECRET
pnpm run deploy

Le déploiement en production se fait avec pnpm run deploy.

Appeler ton worker-email-microservice

Tout autre worker peut maintenant appeler le binding de worker-email-microservice en HTTP pour envoyer un e-mail.

  1. Ajouter le binding à tes apps workers (PAS AU MICROSERVICE)

    Disons que ta page de contact tourne sur le worker landing-page-worker. Ajoute dans sa configuration wrangler.jsonc un binding NOTIFICATION pointant vers worker-email-microservice.

Note : si tu utilises des environnements, déclare le binding propre à l’environnement en plus des bindings de niveau supérieur

json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"services": [
{
"binding": "NOTIFICATION",
"service": "notification",
"environment": "production"
}
],
"env": {
"production": {
"services": [
{
"binding": "NOTIFICATION",
"service": "notification",
"environment": "production"
}
]
}
}
}
  1. Envoyer un e-mail depuis landing-page-worker avec une simple requête HTTP au binding NOTIFICATION.
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const res = await context.cloudflare.env.NOTIFICATION.fetch(
new Request(`https://dummy/send-email`, {
method: "POST",
body: JSON.stringify({
to: {
name: `${firstName} ${lastName}`,
email: email,
},
subject: `${subject}`,
txt,
html,
}),
headers: { "Content-Type": "application/json" },
})
);
if (!res.ok) {
const errorData = await res.json();
return Response.json(
{
success: false,
error: errorData.message || "Failed to send email",
},
{
status: res.status,
}
);
}

Content, tout tourne sans coût supplémentaire :’)
Je me demande juste pourquoi je tape le quota d’e-mails tout le temps, je suis pas si connu que ça 🫠
Voyons le tableau de bord une seconde…

Tableau de bord Resend montrant une activité de bots

Louche, non ?

Mème : pas sûr si c’est une arnaque ou un prince nigérian dans le besoin

J’ai écrit tout un article pour protéger tes formulaires du spam (avec le code qui va avec), je te recommande vraiment de le parcourir pour éviter ce genre de surprise : https://antoninmarxer.hashnode.dev/7-ways-to-stop-form-spam-in-remix-nodejs

En résumé

Au final, ça coûte 1 $/mois : c’est la facture AWS SSM pour stocker une seule clé.
Disons que ça valide le challenge du service d’envoi quasi gratuit.
Tu sais maintenant :

  • Créer un microservice e-mail avec AWS Lambda et la file AWS SQS.

  • Créer un template CloudFormation et un template SAM.

  • Déployer sur AWS de façon reproductible.

  • Publier un événement vers AWS avec @aws-sdk/client-sqs.

  • Créer un microservice dans Cloudflare avec Cloudflare Workers et wrangler.

Dis-moi ce que tu penses de cette stack !
Comment tu envoies les e-mails dans tes apps, et pourquoi avoir choisi cette solution ?

Comments (0)

Log in to leave a comment.

No comments yet. Be the first!

0

Launcher