Email Forwarding with AWS SES and Terraform
Most domains only need one thing from email:
A way for hello@domain.com to reach a real inbox.
Not a mailbox UI. Not calendars. Not another admin panel. Not another monthly subscription.
Just reliable inbound email forwarding.
We built terraform-aws-ses-email-forwarder to solve this in one terraform apply.
The Problem
For simple forwarding use cases, the usual options are often disproportionate to the actual problem.
You either:
- provision full mailbox hosting
- operate mail infrastructure yourself
- or depend on an external forwarding service
when all you really need is:
hello@domain.com → existing inbox
The Solution
Amazon SES can receive email. A Lambda function can forward it. Terraform can wire it all together.
Internet
↓
Amazon SES (inbound receiving)
↓
SES receipt rule
↓
S3 (raw message storage)
↓
Lambda forwarder (Go, ARM64)
↓
Your existing inbox
The module handles everything: domain verification, DKIM, MX records, S3 bucket, receipt rules, Lambda function, and IAM permissions.
Usage
module "email" {
source = "git::https://github.com/ql4b/terraform-aws-ses-email-forwarder.git?ref=v1.0.0"
namespace = "cloudless"
name = "email"
domain_name = "example.com"
recipients = [
"hello@example.com",
"contact@example.com",
]
forward_to = [
"carlo@example.com",
]
route53_zone_id = data.aws_route53_zone.cloudless.zone_id
}
That’s it. After terraform apply, emails to hello@example.com arrive in your existing inbox.
How the Forwarder Works
SES doesn’t pass the full raw email to Lambda directly. The receipt rule stores the message in S3 first, then triggers the forwarder.
The Go Lambda function:
- Reads the raw email from S3
- Adds a
Reply-Toheader with the original sender - Rewrites
Fromto use the verified domain (SES requirement) - Strips headers that break forwarding (DKIM signatures, Message-ID, Return-Path)
- Sends via
SendRawEmailto the destination
This preserves the original message content while satisfying SES sending requirements.
Why Go
The forwarder is a compiled Go binary running on provided.al2023 (ARM64). The binary ships pre-compiled in the module — no build step required for consumers.
Why not Node.js? AWS stopped bundling the SDK in Lambda runtimes after Node 16. A Node.js forwarder would require npm install inside .terraform/modules/ before every apply. That’s impractical for a Terraform module.
Terraform modules work best when consumers do not need an additional build pipeline.
Go gives us:
- Zero runtime dependencies — single static binary
- Ships with the module — no build step for consumers
- ~4.7MB zip — fast cold starts
- ARM64 — cheapest Lambda pricing
Design Decisions
No SPF record creation. Domains often have existing TXT records (Google site verification, etc.) that would conflict. SPF is documented as a manual recommendation instead.
Pre-compiled binary. The module consumer experience is just terraform apply. The Makefile exists for maintainers who need to rebuild.
Single receipt rule. S3 action + Lambda action in one rule. No validation step, no DynamoDB, no API Gateway. This module does one thing.
CloudPosse context. Consistent naming via cloudposse/label/null — same pattern as our other modules.
What It Costs
At low volume (< 1000 emails/month):
- SES receiving: free (first 1000 messages)
- SES sending (forwarding): $0.10 per 1000 emails
- S3 storage: negligible (messages expire after 30 days)
- Lambda: negligible (milliseconds per invocation)
Effectively free for most use cases.
Operational Notes
SES receiving is regional.
The module creates the MX record pointing to the SES inbound endpoint for the selected region automatically.
Raw inbound messages are stored temporarily in S3 before forwarding. By default, messages expire after 30 days through an S3 lifecycle policy.
For low-volume project addresses, the operational footprint is effectively zero.
When to Use This
Good for:
- Contact address for a static website
- Project inbox forwarded to a team mailbox
- Domain aliases that forward to one or more real inboxes
- Lightweight inbound email for small services
Not for:
- Full email hosting
- High-volume transactional email
- Complex routing rules
- Inbox management with read/unread state
Alternatives Considered
There are already many ways to solve email forwarding.
Some good alternatives include:
- Cloudflare Email Routing
- SimpleLogin
- Google Workspace
- Fastmail
- Self-hosted Postfix setups
The goal of this module is not to replace those systems.
It exists for a very specific case:
- you already use AWS
- you already manage infrastructure with Terraform
- you want inbound email forwarding as infrastructure
- and you want the operational surface to remain minimal
SES Receiving Region
SES inbound email receiving is available in most AWS regions. Deploy this module in a region that supports SES receiving and the MX record will point to the corresponding regional endpoint automatically.
The Bigger Picture
Most small projects do not need platforms.
They need a few reliable building blocks:
- DNS
- storage
- inbound email
- automation
- observability
Cloud infrastructure is most effective when these pieces remain small, composable, and understandable.
This module exists because we needed hello@ourdomain.com to work without introducing an entire mail platform into the system.
Now it does.
terraform-aws-ses-email-forwarder is open source and available on GitHub.