Skip to content

Instantly share code, notes, and snippets.

@allan-gar2x
Created April 3, 2026 07:39
Show Gist options
  • Select an option

  • Save allan-gar2x/7b2ae4e58fcb4bf5a23bdad1a1b00205 to your computer and use it in GitHub Desktop.

Select an option

Save allan-gar2x/7b2ae4e58fcb4bf5a23bdad1a1b00205 to your computer and use it in GitHub Desktop.
Starlight Practice — AWS SES Full Implementation Plan (Tasks 1-5)

Starlight Practice — AWS SES Full Implementation Plan

Context

Building the messaging feature — appointment reminders, billing notices, wellness check notifications, and patient onboarding emails. AWS SES must be configured before the dev team can wire it into the backend.

  • Environments: Staging (dev) + Production
  • AWS Region: us-east-2
  • IaC Tool: OpenTofu (existing b4m-infra repo — NOT Pulumi/SST)

Pre-Implementation Verification (confirmed 2026-04-03)

1. Route53 Zones — Already in OpenTofu

Account Domain Zone ID File
starlightpractice-prod (524471258033) starlightpractice.com Z03036173FL99T10J98KK accounts/starlightpractice-prod/main.tf
starlightpractice-dev (421807902331) dev.starlightpractice.com Z028466637NTWD1K6UX8Z accounts/starlightpractice-dev/main.tf

Both zones are already managed in OpenTofu state in b4m-infra. SES DKIM CNAMEs reference them directly — no new zones needed.

2. IaC Tool — OpenTofu (not Pulumi)

The starlight project uses OpenTofu exclusively in b4m-infra. Pulumi is NOT used here. OpenTofu is the correct tool for all automatable tasks.

3. SES Status — Both Accounts in Sandbox

Account Sending Enabled Production Access Existing Identities
starlightpractice-dev ✅ Yes ❌ Sandbox only None
starlightpractice-prod ✅ Yes ❌ Sandbox only None

Both accounts need the manual sandbox removal request (Task 2).


Task Summary

Task Method Notes
1. Domain verification + DKIM + MAIL FROM OpenTofu ses.tf in each account
2. Sandbox → Production access AWS Console (manual) No API exists for this
3. Sending identity (notifications@) OpenTofu Included in ses.tf
4. IAM permissions for Lambda OpenTofu Included in ses.tf, gated by variable
5. SNS + config set + CloudWatch alarms OpenTofu Included in ses.tf

Files to Create/Modify

File Action
accounts/starlightpractice-prod/ses.tf Create new — full SES for prod
accounts/starlightpractice-dev/ses.tf Create new — full SES for dev
accounts/starlightpractice-dev/main.tf Edit existing — add us_east_1 provider alias

Why the us_east_1 provider alias?

AWS publishes SES reputation metrics (Reputation.BounceRate, Reputation.ComplaintRate) to CloudWatch in us-east-1 only, regardless of which region SES is deployed in. The CloudWatch alarm resources require a provider pointed at us-east-1. The prod account already has this alias — dev is missing it and needs it added.


Why Apply Prod Before Dev?

  • starlightpractice.com (root domain) lives in the prod account's Route53
  • dev.starlightpractice.com is a subdomain delegated from prod (see module "dev_delegation" in prod's main.tf)
  • Each AWS account verifies its own SES identity independently — prod cannot share its verification with dev
  • Prod is applied first to confirm the root domain and DNS delegation are healthy before layering dev on top
Account Sends from Verifies in
starlightpractice-prod notifications@starlightpractice.com SES account 524471258033
starlightpractice-dev notifications@dev.starlightpractice.com SES account 421807902331

Deployment Order

Step 1 — Apply prod

cd accounts/starlightpractice-prod
tofu init
tofu plan
tofu apply

Creates: SES domain identity, 3 DKIM CNAMEs, MAIL FROM records, sending identity, config set, SNS topic, CloudWatch alarms.

Step 2 — Apply dev

cd accounts/starlightpractice-dev
tofu init
tofu plan
tofu apply

Same as prod but for dev.starlightpractice.com.

Step 3 — Wait for DKIM verification (5–30 minutes)

Poll until Status = "SUCCESS":

# Prod
aws sesv2 get-email-identity \
  --email-identity starlightpractice.com \
  --region us-east-2 \
  --profile starlightpractice-prod \
  --query 'DkimAttributes.Status'

# Dev
aws sesv2 get-email-identity \
  --email-identity dev.starlightpractice.com \
  --region us-east-2 \
  --profile starlightpractice-dev \
  --query 'DkimAttributes.Status'

Step 4 — Submit sandbox removal requests (AWS Console)

See task-2-sandbox-removal.md. Do this for both accounts once DKIM = "SUCCESS". AWS typically responds within 24 hours.

Step 5 — Wire Lambda IAM once SST app is deployed

Once the Lambda execution role name is known, run:

# Prod
cd accounts/starlightpractice-prod
tofu apply -var='lambda_role_name=<role-name-from-sst>'

# Dev
cd accounts/starlightpractice-dev
tofu apply -var='lambda_role_name=<role-name-from-sst>'

Step 6 — Dev team instruction

Every SendEmail SDK call must include the configuration set:

  • Prod: ConfigurationSetName = "starlight-prod"
  • Dev: ConfigurationSetName = "starlight-dev"

Important Notes

  • HIPAA BAA: Ensure a signed AWS Business Associate Agreement is activated on both accounts before sending any PHI through SES. This is a manual checkbox under Account Settings → HIPAA eligibility in the AWS Console.

  • Lambda role: Task 4 (IAM) is gated by the lambda_role_name variable. Leave it empty on first apply — the resource uses count = 0 when the variable is blank, so it safely skips without error. Fill it in once the SST app is deployed.

  • Configuration set is required: SES bounce/complaint suppression and CloudWatch tracking only work if the dev team passes ConfigurationSetName in every send call. Without it, sends are untracked and the suppression list is bypassed.

# ============================================================================
# accounts/starlightpractice-dev/main.tf — ADDITION ONLY
# ============================================================================
# Add this provider block to the EXISTING main.tf in starlightpractice-dev.
# Do NOT replace the existing file — just append/insert this block.
#
# WHY: AWS publishes SES reputation metrics (Reputation.BounceRate,
# Reputation.ComplaintRate) to CloudWatch in us-east-1 ONLY, regardless
# of which region SES is deployed in. The CloudWatch alarm resources in
# ses.tf use `provider = aws.us_east_1` and require this alias to exist.
#
# NOTE: The prod account (starlightpractice-prod/main.tf) already has this
# alias defined — no change needed there.
#
# Insert after the existing:
# provider "aws" { region = "us-east-2", profile = "starlightpractice-dev" }
# ============================================================================
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
profile = "starlightpractice-dev"
}
# ============================================================================
# accounts/starlightpractice-dev/ses.tf
# ============================================================================
# SES full implementation for dev.starlightpractice.com (staging/dev)
# Tasks: 1 (domain + DKIM + MAIL FROM), 3 (sending identity),
# 4 (IAM for Lambda), 5 (config set + SNS + CloudWatch)
#
# Prerequisites:
# - aws_route53_zone.main already exists in main.tf (zone: dev.starlightpractice.com)
# - Add us_east_1 provider alias to main.tf (see starlightpractice-dev-main-addition.tf)
# - Run `tofu apply` before submitting sandbox removal request (Task 2)
# ============================================================================
# ============================================================================
# Task 1: SES Domain Identity + DKIM
# ============================================================================
resource "aws_sesv2_email_identity" "domain" {
email_identity = "dev.starlightpractice.com"
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
dkim_signing_attributes {
next_signing_key_length = "RSA_2048_BIT"
}
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
# Publish 3 DKIM CNAME records into the existing Route53 zone
resource "aws_route53_record" "ses_dkim_cname" {
for_each = toset(aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens)
zone_id = aws_route53_zone.main.zone_id
name = "${each.value}._domainkey.dev.starlightpractice.com"
type = "CNAME"
ttl = 300
records = ["${each.value}.dkim.amazonses.com"]
}
# ============================================================================
# Task 1b: MAIL FROM Domain (improves deliverability / DMARC alignment)
# ============================================================================
resource "aws_sesv2_email_identity_mail_from_attributes" "domain" {
email_identity = aws_sesv2_email_identity.domain.email_identity
mail_from_domain = "mail.dev.starlightpractice.com"
behavior_on_mx_failure = "USE_DEFAULT_VALUE"
}
resource "aws_route53_record" "ses_mail_from_mx" {
zone_id = aws_route53_zone.main.zone_id
name = "mail.dev.starlightpractice.com"
type = "MX"
ttl = 300
records = ["10 feedback-smtp.us-east-2.amazonses.com"]
}
resource "aws_route53_record" "ses_mail_from_spf" {
zone_id = aws_route53_zone.main.zone_id
name = "mail.dev.starlightpractice.com"
type = "TXT"
ttl = 300
records = ["\"v=spf1 include:amazonses.com ~all\""]
}
# ============================================================================
# Task 3: Sending Identity
# ============================================================================
resource "aws_sesv2_email_identity" "from_address" {
email_identity = "notifications@dev.starlightpractice.com"
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
depends_on = [aws_sesv2_email_identity.domain]
}
# ============================================================================
# Task 4: IAM Permissions for Lambda
# ============================================================================
variable "lambda_role_name" {
description = "Name of the Lambda execution role that needs SES send permissions. Populate once the SST app is deployed. Leave blank to skip on first apply."
type = string
default = ""
}
resource "aws_iam_role_policy" "lambda_ses_send" {
count = var.lambda_role_name != "" ? 1 : 0
name = "ses-send-dev"
role = var.lambda_role_name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SESSendFromVerifiedDomain"
Effect = "Allow"
Action = [
"ses:SendEmail",
"ses:SendRawEmail"
]
Resource = [
"arn:aws:ses:us-east-2:421807902331:identity/dev.starlightpractice.com"
]
},
{
Sid = "SESGetQuota"
Effect = "Allow"
Action = ["ses:GetSendQuota"]
Resource = ["*"]
}
]
})
}
# ============================================================================
# Task 5: Configuration Set + SNS + CloudWatch Monitoring
# ============================================================================
resource "aws_sesv2_configuration_set" "main" {
configuration_set_name = "starlight-dev"
sending_options {
sending_enabled = true
}
suppression_options {
suppressed_reasons = ["BOUNCE", "COMPLAINT"]
}
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
resource "aws_sns_topic" "ses_events" {
name = "ses-events-dev"
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
variable "alarm_email" {
description = "Email address to receive SES bounce/complaint alarm notifications."
type = string
default = "ops@starlightpractice.com"
}
resource "aws_sns_topic_subscription" "ses_events_email" {
topic_arn = aws_sns_topic.ses_events.arn
protocol = "email"
endpoint = var.alarm_email
}
resource "aws_sesv2_configuration_set_event_destination" "sns" {
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
event_destination_name = "sns-bounce-complaint"
event_destination {
enabled = true
matching_event_types = ["BOUNCE", "COMPLAINT", "DELIVERY_DELAY"]
sns_destination {
topic_arn = aws_sns_topic.ses_events.arn
}
}
}
# SES reputation metrics are published to us-east-1 by AWS regardless of sending region.
# Requires the us_east_1 provider alias — see starlightpractice-dev-main-addition.tf
resource "aws_cloudwatch_metric_alarm" "ses_bounce_rate" {
provider = aws.us_east_1
alarm_name = "ses-bounce-rate-dev"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Reputation.BounceRate"
namespace = "AWS/SES"
period = 900
statistic = "Average"
threshold = 0.05
alarm_description = "SES bounce rate exceeded 5% in dev. AWS suspends sending at 10%."
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.ses_events.arn]
ok_actions = [aws_sns_topic.ses_events.arn]
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
resource "aws_cloudwatch_metric_alarm" "ses_complaint_rate" {
provider = aws.us_east_1
alarm_name = "ses-complaint-rate-dev"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Reputation.ComplaintRate"
namespace = "AWS/SES"
period = 900
statistic = "Average"
threshold = 0.001
alarm_description = "SES complaint rate exceeded 0.1% in dev. AWS suspends sending at 0.5%."
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.ses_events.arn]
tags = {
Environment = "dev"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
# ============================================================================
# Outputs
# ============================================================================
output "ses_domain_identity_arn" {
value = aws_sesv2_email_identity.domain.arn
description = "SES domain identity ARN — use in Lambda IAM policies"
}
output "ses_configuration_set_name" {
value = aws_sesv2_configuration_set.main.configuration_set_name
description = "Pass as ConfigurationSetName in every Lambda SendEmail call"
}
output "ses_events_sns_topic_arn" {
value = aws_sns_topic.ses_events.arn
description = "SNS topic ARN for SES bounce/complaint events"
}
# ============================================================================
# accounts/starlightpractice-prod/ses.tf
# ============================================================================
# SES full implementation for starlightpractice.com (production)
# Tasks: 1 (domain + DKIM + MAIL FROM), 3 (sending identity),
# 4 (IAM for Lambda), 5 (config set + SNS + CloudWatch)
#
# Prerequisites:
# - aws_route53_zone.root already exists in main.tf (zone: starlightpractice.com)
# - provider "aws" { alias = "us_east_1" } already exists in main.tf
# - Run `tofu apply` before submitting sandbox removal request (Task 2)
# ============================================================================
# ============================================================================
# Task 1: SES Domain Identity + DKIM
# ============================================================================
resource "aws_sesv2_email_identity" "domain" {
email_identity = "starlightpractice.com"
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
dkim_signing_attributes {
next_signing_key_length = "RSA_2048_BIT"
}
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
# Publish 3 DKIM CNAME records into the existing Route53 zone
resource "aws_route53_record" "ses_dkim_cname" {
for_each = toset(aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens)
zone_id = aws_route53_zone.root.zone_id
name = "${each.value}._domainkey.starlightpractice.com"
type = "CNAME"
ttl = 300
records = ["${each.value}.dkim.amazonses.com"]
}
# ============================================================================
# Task 1b: MAIL FROM Domain (improves deliverability / DMARC alignment)
# ============================================================================
resource "aws_sesv2_email_identity_mail_from_attributes" "domain" {
email_identity = aws_sesv2_email_identity.domain.email_identity
mail_from_domain = "mail.starlightpractice.com"
behavior_on_mx_failure = "USE_DEFAULT_VALUE"
}
resource "aws_route53_record" "ses_mail_from_mx" {
zone_id = aws_route53_zone.root.zone_id
name = "mail.starlightpractice.com"
type = "MX"
ttl = 300
records = ["10 feedback-smtp.us-east-2.amazonses.com"]
}
resource "aws_route53_record" "ses_mail_from_spf" {
zone_id = aws_route53_zone.root.zone_id
name = "mail.starlightpractice.com"
type = "TXT"
ttl = 300
records = ["\"v=spf1 include:amazonses.com ~all\""]
}
# ============================================================================
# Task 3: Sending Identity
# ============================================================================
resource "aws_sesv2_email_identity" "from_address" {
email_identity = "notifications@starlightpractice.com"
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
depends_on = [aws_sesv2_email_identity.domain]
}
# ============================================================================
# Task 4: IAM Permissions for Lambda
# ============================================================================
variable "lambda_role_name" {
description = "Name of the Lambda execution role that needs SES send permissions. Populate once the SST app is deployed. Leave blank to skip on first apply."
type = string
default = ""
}
resource "aws_iam_role_policy" "lambda_ses_send" {
count = var.lambda_role_name != "" ? 1 : 0
name = "ses-send-prod"
role = var.lambda_role_name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SESSendFromVerifiedDomain"
Effect = "Allow"
Action = [
"ses:SendEmail",
"ses:SendRawEmail"
]
Resource = [
"arn:aws:ses:us-east-2:524471258033:identity/starlightpractice.com"
]
},
{
Sid = "SESGetQuota"
Effect = "Allow"
Action = ["ses:GetSendQuota"]
Resource = ["*"]
}
]
})
}
# ============================================================================
# Task 5: Configuration Set + SNS + CloudWatch Monitoring
# ============================================================================
resource "aws_sesv2_configuration_set" "main" {
configuration_set_name = "starlight-prod"
sending_options {
sending_enabled = true
}
suppression_options {
# Auto-suppress addresses that hard-bounce or complain.
# Critical for maintaining sender reputation.
suppressed_reasons = ["BOUNCE", "COMPLAINT"]
}
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
resource "aws_sns_topic" "ses_events" {
name = "ses-events-prod"
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
variable "alarm_email" {
description = "Email address to receive SES bounce/complaint alarm notifications."
type = string
default = "ops@starlightpractice.com"
}
resource "aws_sns_topic_subscription" "ses_events_email" {
topic_arn = aws_sns_topic.ses_events.arn
protocol = "email"
endpoint = var.alarm_email
}
resource "aws_sesv2_configuration_set_event_destination" "sns" {
configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
event_destination_name = "sns-bounce-complaint"
event_destination {
enabled = true
matching_event_types = ["BOUNCE", "COMPLAINT", "DELIVERY_DELAY"]
sns_destination {
topic_arn = aws_sns_topic.ses_events.arn
}
}
}
# SES reputation metrics are published to us-east-1 by AWS regardless of sending region.
# Prod already has the us_east_1 provider alias in main.tf — no change needed there.
resource "aws_cloudwatch_metric_alarm" "ses_bounce_rate" {
provider = aws.us_east_1
alarm_name = "ses-bounce-rate-prod"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Reputation.BounceRate"
namespace = "AWS/SES"
period = 900
statistic = "Average"
threshold = 0.05 # 5% — AWS auto-suspends at 10%
alarm_description = "SES bounce rate exceeded 5% in prod. AWS suspends sending at 10%."
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.ses_events.arn]
ok_actions = [aws_sns_topic.ses_events.arn]
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
resource "aws_cloudwatch_metric_alarm" "ses_complaint_rate" {
provider = aws.us_east_1
alarm_name = "ses-complaint-rate-prod"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Reputation.ComplaintRate"
namespace = "AWS/SES"
period = 900
statistic = "Average"
threshold = 0.001 # 0.1% — AWS auto-suspends at 0.5%
alarm_description = "SES complaint rate exceeded 0.1% in prod. AWS suspends sending at 0.5%."
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.ses_events.arn]
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
# ============================================================================
# Outputs
# ============================================================================
output "ses_domain_identity_arn" {
value = aws_sesv2_email_identity.domain.arn
description = "SES domain identity ARN — use in Lambda IAM policies"
}
output "ses_configuration_set_name" {
value = aws_sesv2_configuration_set.main.configuration_set_name
description = "Pass as ConfigurationSetName in every Lambda SendEmail call"
}
output "ses_events_sns_topic_arn" {
value = aws_sns_topic.ses_events.arn
description = "SNS topic ARN for SES bounce/complaint events"
}

Task 2: SES Sandbox Removal — Manual Steps (AWS Console)

Why this cannot be automated

AWS requires a human to submit a support case explaining your use case, sending volume, and how you handle bounces/complaints. There is no API, no OpenTofu resource, and no CLI command that submits this request on your behalf.

Timing

Do this AFTER tofu apply completes on both accounts. AWS reviewers check that your domain is already verified before approving. A verified domain significantly improves approval chances and speed.

Steps (repeat for BOTH accounts)

Account 1: starlightpractice-dev (421807902331)

Account 2: starlightpractice-prod (524471258033)

  1. Sign in to the AWS Console for the account

  2. Switch region to us-east-2

  3. Navigate to SES → Account dashboard

  4. Click "Request production access" (banner at the top of the page)

  5. Fill in the form:

    Field Value
    Mail type TRANSACTIONAL
    Website URL https://starlightpractice.com

    Use case description:

    "HIPAA-compliant pediatric medical practice sending transactional emails — appointment reminders, billing notices, wellness check notifications, and patient onboarding to families. All recipients have explicitly opted in through the patient portal registration flow. We maintain unsubscribe mechanisms and honor all opt-outs within 10 business days."

  6. Submit — AWS typically responds within 24 hours

  7. Repeat for the second account

Current Status (verified 2026-04-03)

Account SendingEnabled ProductionAccess
starlightpractice-dev (421807902331) ✅ Yes ❌ Sandbox — action required
starlightpractice-prod (524471258033) ✅ Yes ❌ Sandbox — action required

HIPAA Note

Before sending any PHI through SES, ensure a signed AWS Business Associate Agreement (BAA) is activated on both accounts: AWS Console → Account Settings → HIPAA eligibility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment