Skip to content
by skunxicat

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:

  1. Reads the raw email from S3
  2. Adds a Reply-To header with the original sender
  3. Rewrites From to use the verified domain (SES requirement)
  4. Strips headers that break forwarding (DKIM signatures, Message-ID, Return-Path)
  5. Sends via SendRawEmail to 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.