E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio

Quero te levar do zero até o primeiro e-mail de verificação enviado pela AWS, usando Terraform para criar a infra e AWS Lambda (Node.js com AWS SDK v3) para enviar. A ideia é simples: você provisiona tudo como código, empacota uma pequena função de env…


This content originally appeared on DEV Community and was authored by Cláudio Filipe Lima Rapôso

Quero te levar do zero até o primeiro e-mail de verificação enviado pela AWS, usando Terraform para criar a infra e AWS Lambda (Node.js com AWS SDK v3) para enviar. A ideia é simples: você provisiona tudo como código, empacota uma pequena função de envio e testa em minutos.

Observação importante sobre o SES: contas novas começam no sandbox. Nesse modo, você só consegue enviar para endereços verificados (remetente e destinatário) e com limites mais baixos. Para enviar para qualquer pessoa, é preciso solicitar saída do sandbox. Vou te mostrar como validar identidades e como testar mesmo no sandbox. (AWS Documentation)

O problema que essa solução resolve

Quase toda app precisa confirmar se um e-mail realmente pertence ao usuário. Em vez de acoplar envio de e-mail no backend principal, vamos isolar a responsabilidade em uma Lambda enxuta, com boas práticas de engenharia (SOLID/DRY, separação de camadas) e infra como código. Resultado: baixo acoplamento, escala automática e governança simples.

O que vamos montar

  • Um Email Identity no Amazon SES (o remetente).
  • Uma IAM Role mínima para a Lambda.
  • Uma Lambda em Node.js 20 que usa AWS SDK v3 (SESv2) para enviar.
  • Opcional: um Function URL para chamar a Lambda por HTTP e testar com curl/Postman. (Terraform Registry)

Pré-requisitos

  • Conta AWS com aws configure pronto.
  • Terraform ≥ 1.5.
  • Node.js 20 + npm.
  • Região do SES suportada (ex.: us-east-1).

Estrutura de pastas

ses-verify/
  app/
    package.json
    tsconfig.json
    src/
      config.ts
      dto.ts
      email/
        SesEmailClient.ts
        EmailService.ts
      handler.ts
  build/        # gerado pelo script
  infra/
    main.tf
    variables.tf
    outputs.tf

Passo 1 — Infra com Terraform (SES, Lambda, IAM e Function URL)

Crie os arquivos abaixo em infra/.

variables.tf

variable "project_name" {
  type    = string
  default = "ses-verify"
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "sender_email" {
  type = string
}

variable "lambda_package_path" {
  type    = string
  default = "../build/lambda.zip"
}

variable "expose_function_url" {
  type    = bool
  default = false
}

variable "function_url_auth_type" {
  type    = string
  default = "NONE"
}

main.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_sesv2_email_identity" "sender" {
  email_identity = var.sender_email
}

data "aws_iam_policy_document" "lambda_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "lambda" {
  name               = "${var.project_name}-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

resource "aws_iam_role_policy_attachment" "basic_logs" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "aws_iam_policy_document" "ses_send" {
  statement {
    effect    = "Allow"
    actions   = ["ses:SendEmail"]
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "ses_send" {
  name   = "${var.project_name}-ses-send"
  role   = aws_iam_role.lambda.id
  policy = data.aws_iam_policy_document.ses_send.json
}

resource "aws_lambda_function" "send_verification" {
  function_name    = "${var.project_name}-send"
  role             = aws_iam_role.lambda.arn
  handler          = "dist/handler.handler"
  filename         = var.lambda_package_path
  source_code_hash = filebase64sha256(var.lambda_package_path)
  runtime          = "nodejs20.x"
  timeout          = 10
  environment {
    variables = {
      SENDER_EMAIL = var.sender_email
      SES_REGION   = var.aws_region
    }
  }
}

resource "aws_lambda_function_url" "this" {
  count               = var.expose_function_url ? 1 : 0
  function_name       = aws_lambda_function.send_verification.function_name
  authorization_type  = var.function_url_auth_type
  cors {
    allow_origins = ["*"]
    allow_methods = ["POST", "OPTIONS"]
    allow_headers = ["content-type"]
  }
}

Refs úteis: aws_lambda_function, aws_lambda_function_url e política básica de logs para Lambda. (Terraform Registry, AWS Documentation)

outputs.tf

output "lambda_function_name" {
  value = aws_lambda_function.send_verification.function_name
}

output "lambda_function_arn" {
  value = aws_lambda_function.send_verification.arn
}

output "function_url" {
  value       = try(aws_lambda_function_url.this[0].function_url, null)
  description = "URL pública opcional para testes"
}

Passo 2 — Lambda em Node.js (AWS SDK v3 / SESv2)

Abaixo, um design simples, mas com separação de responsabilidades:

  • SesEmailClient encapsula o client do SESv2.
  • EmailService define o caso de uso de envio de verificação.
  • config centraliza variáveis de ambiente.
  • handler expõe a função Lambda.

app/package.json

{
  "name": "ses-verify-lambda",
  "version": "1.0.0",
  "private": true,
  "main": "dist/handler.js",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "package": "mkdir -p ../build && zip -r ../build/lambda.zip dist node_modules package.json"
  },
  "dependencies": {
    "@aws-sdk/client-sesv2": "^3.600.0"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.136",
    "typescript": "^5.5.4"
  }
}

app/tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

app/src/config.ts

export const env = {
  region: process.env.SES_REGION || process.env.AWS_REGION || "us-east-1",
  sender: process.env.SENDER_EMAIL || ""
};

app/src/dto.ts

export type SendVerificationInput = {
  to: string;
  code: string;
  subject?: string;
  link?: string;
};

app/src/email/SesEmailClient.ts

import { SESv2Client, SendEmailCommand, SendEmailCommandInput } from "@aws-sdk/client-sesv2";

export class SesEmailClient {
  private readonly client: SESv2Client;

  constructor(region: string) {
    this.client = new SESv2Client({ region });
  }

  send(input: SendEmailCommandInput) {
    return this.client.send(new SendEmailCommand(input));
  }
}

app/src/email/EmailService.ts

import { SesEmailClient } from "./SesEmailClient";
import { SendVerificationInput } from "../dto";

export class EmailService {
  constructor(private readonly ses: SesEmailClient, private readonly sender: string) {}

  async sendVerification({ to, code, subject, link }: SendVerificationInput) {
    if (!this.sender) throw new Error("Missing sender");
    if (!to || !code) throw new Error("Invalid payload");
    const subj = subject || "Verifique seu e-mail";
    const html = this.buildHtml(code, link);
    const text = this.buildText(code, link);
    return this.ses.send({
      FromEmailAddress: this.sender,
      Destination: { ToAddresses: [to] },
      Content: {
        Simple: {
          Subject: { Data: subj, Charset: "UTF-8" },
          Body: {
            Html: { Data: html, Charset: "UTF-8" },
            Text: { Data: text, Charset: "UTF-8" }
          }
        }
      }
    });
  }

  private buildHtml(code: string, link?: string) {
    const callToAction = link ? `<p><a href="${link}">Confirmar conta</a></p>` : "";
    return `<h1>Seu código</h1><p>${code}</p>${callToAction}`;
  }

  private buildText(code: string, link?: string) {
    const callToAction = link ? `\n\nConfirmar: ${link}` : "";
    return `Seu código: ${code}${callToAction}`;
  }
}

app/src/handler.ts

import { env } from "./config";
import { EmailService } from "./email/EmailService";
import { SesEmailClient } from "./email/SesEmailClient";
import { SendVerificationInput } from "./dto";

const service = new EmailService(new SesEmailClient(env.region), env.sender);

export const handler = async (event: any) => {
  const body = typeof event?.body === "string" ? JSON.parse(event.body) : event;
  const payload = body as SendVerificationInput;
  await service.sendVerification(payload);
  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ ok: true })
  };
};

A operação usada é SendEmail do SESv2; no SDK v3, montamos Destination, Content.Simple e FromEmailAddress como no exemplo acima. (AWS Documentation)

Passo 3 — Build do código e empacotamento

Dentro de app/:

npm ci
npm run build
npm run package

Isso gera build/lambda.zip, que o Terraform referenciará.

Passo 4 — Deploy com Terraform

Na raiz do projeto:

terraform -chdir=infra init
terraform -chdir=infra apply \
  -var='aws_region=us-east-1' \
  -var='sender_email=seu-remetente@exemplo.com' \
  -var='lambda_package_path=../build/lambda.zip' \
  -var='expose_function_url=true'

O SES vai enviar um e-mail de verificação para o remetente informado. Clique no link recebido para confirmar o Email Identity antes de testar o envio. Em sandbox, o destinatário também precisa estar verificado (ou use o mailbox simulator). Para liberar envio geral, faça o pedido de produção. (AWS Documentation)

Passo 5 — Testes

Opção A: invocar direto a Lambda (AWS CLI)

FUNC_NAME=$(terraform -chdir=infra output -raw lambda_function_name)

aws lambda invoke \
  --function-name "$FUNC_NAME" \
  --cli-binary-format raw-in-base64-out \
  --payload '{"to":"destinatario@exemplo.com","code":"123456","subject":"Verifique seu e-mail","link":"https://minhaapp/verify?code=123456"}' \
  /dev/stdout

Opção B: chamar via Function URL (curl/Postman)

Se você ativou expose_function_url=true, capture a URL:

URL=$(terraform -chdir=infra output -raw function_url)
curl -s -X POST "$URL" -H 'content-type: application/json' \
  -d '{"to":"destinatario@exemplo.com","code":"789012"}'

Function URLs são um atalho elegante para testar HTTP sem API Gateway. Para produção, considere autenticação (AWS_IAM), WAF/CloudFront e políticas mais restritivas. (Terraform Registry, Håkon Eriksen Drange - Perspectives)

Se tudo deu certo, o e-mail chegará no destino permitido pelo seu status do SES.

Próximos passos e boas práticas

  • Sair do sandbox quando estiver pronto para enviar a qualquer domínio. (AWS Documentation)
  • Domínio verificado + DKIM/DMARC para reputação e entregabilidade melhores (em vez de só endereço). (AWS Documentation)
  • Templates e métricas com Configuration Sets, IP pools gerenciados, etc., se precisar escalar e observar. (Stack Overflow)
  • Segurança: se usar Function URL em produção, avalie AWS_IAM, WAF e CDN com origem na URL. (Håkon Eriksen Drange - Perspectives)


This content originally appeared on DEV Community and was authored by Cláudio Filipe Lima Rapôso


Print Share Comment Cite Upload Translate Updates
APA

Cláudio Filipe Lima Rapôso | Sciencx (2025-08-18T21:43:30+00:00) E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio. Retrieved from https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/

MLA
" » E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio." Cláudio Filipe Lima Rapôso | Sciencx - Monday August 18, 2025, https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/
HARVARD
Cláudio Filipe Lima Rapôso | Sciencx Monday August 18, 2025 » E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio., viewed ,<https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/>
VANCOUVER
Cláudio Filipe Lima Rapôso | Sciencx - » E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/
CHICAGO
" » E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio." Cláudio Filipe Lima Rapôso | Sciencx - Accessed . https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/
IEEE
" » E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio." Cláudio Filipe Lima Rapôso | Sciencx [Online]. Available: https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/. [Accessed: ]
rf:citation
» E-mails de verificação com AWS SES + Lambda (Node.js) e Terraform — do zero ao envio | Cláudio Filipe Lima Rapôso | Sciencx | https://www.scien.cx/2025/08/18/e-mails-de-verificacao-com-aws-ses-lambda-node-js-e-terraform-do-zero-ao-envio/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.