Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

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

Starlight Practice — AWS SES Implementation Plan

Context

We're building the messaging feature — appointment reminders, billing notices, wellness check notifications, and patient onboarding emails. We need AWS SES configured before the dev team can wire it into the backend.

  • Environments: Staging + Production
  • AWS Region: us-east-2 (same as existing infra)
  • IaC Tool: OpenTofu (b4m-infra repo)

Pre-Implementation Findings (verified 2026-04-03)

Check Finding
Route53 zones Already in OpenTofu state in b4m-infra
prod zone starlightpractice.com → ID: Z03036173FL99T10J98KK
staging zone dev.starlightpractice.com → ID: Z028466637NTWD1K6UX8Z
IaC tool OpenTofu (not Pulumi/SST — starlight is separate from lumina5)
SES status — prod SendingEnabled ✅, ProductionAccess ❌ (sandbox)
SES status — staging SendingEnabled ✅, ProductionAccess ❌ (sandbox)
Existing SES identities None on either account
prod us_east_1 provider Already exists in main.tf
staging us_east_1 provider Missing — must be added to main.tf ⚠️

Task 1: Domain Verification

What: Verify starlightpractice.com in SES (us-east-2), add the 3 DKIM CNAME records to DNS, enable MAIL FROM domain, and confirm both staging and production accounts have access.

How: OpenTofu — ses.tf in each account directory.

Files:

  • accounts/starlightpractice-prod/ses.tf (see starlightpractice-prod-ses.tf)
  • accounts/starlightpractice-dev/ses.tf (see starlightpractice-dev-ses.tf)

What OpenTofu does for this task:

  • Creates the SES domain identity (aws_sesv2_email_identity)
  • Generates 3 DKIM tokens and publishes CNAME records into the existing Route53 zone
  • Configures MAIL FROM subdomain (mail.starlightpractice.com) for DMARC alignment
  • Adds MX + SPF TXT records for the MAIL FROM subdomain

Sending domains per environment:

Environment Domain Sends from
Production starlightpractice.com notifications@starlightpractice.com
Staging dev.starlightpractice.com notifications@dev.starlightpractice.com

Each AWS account verifies its own SES identity independently — prod cannot share its verification with staging.

Verify DKIM status (poll after apply, wait 5–30 min):

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

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

Task 2: Request Production Access

What: SES starts in sandbox mode (can only send to verified emails). We need production access for both staging and production accounts.

How: Manual — AWS Console only. No API, OpenTofu resource, or CLI command exists for this.

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

Steps (repeat for BOTH accounts):

  1. Sign in to AWS Console for the account → region us-east-2

  2. Navigate to SES → Account dashboard

  3. Click "Request production access"

  4. Fill in:

    Field Value
    Mail type TRANSACTIONAL
    Website https://starlightpractice.com
    Use case See below

    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."

  5. Submit — AWS typically responds within 24 hours

  6. Repeat for the second account

Accounts:

Account Account ID Current Status
starlightpractice-prod 524471258033 ❌ Sandbox
starlightpractice-dev 421807902331 ❌ Sandbox

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


Task 3: Sending Identity

What: Configure the from address (notifications@starlightpractice.com or noreply@). Reply-to is practice-specific (configurable per tenant, stored in practice_settings).

How: OpenTofu — included in ses.tf for each account.

What OpenTofu does for this task:

  • Creates an explicit email identity for the from address (aws_sesv2_email_identity)
  • Attaches it to the configuration set so sends are tracked
  • The domain identity from Task 1 covers all addresses at that domain, but an explicit identity allows per-address configuration set enforcement

Task 4: IAM Permissions

What: Lambda execution role needs ses:SendEmail and ses:SendRawEmail permissions, scoped to the verified identity ARN: arn:aws:ses:us-east-2:*:identity/starlightpractice.com

How: OpenTofu — included in ses.tf. Gated by a variable so it can be skipped on first apply (Lambda role doesn't exist yet — no SST app deployed for starlight).

Usage:

# Once the SST Lambda role name is known, re-apply with:
tofu apply -var='lambda_role_name=<role-name-from-sst>'

The resource uses count = var.lambda_role_name != "" ? 1 : 0 — it safely skips on first apply when the variable is left blank.


Task 5: Monitoring (nice to have)

What:

  • SNS topic for bounce/complaint notifications
  • SES configuration set for tracking sends/bounces/complaints
  • CloudWatch alarm if bounce rate exceeds 5%

How: OpenTofu — included in ses.tf for each account.

What OpenTofu does for this task:

  • Creates a SES configuration set (starlight-prod / starlight-dev) with suppression enabled for bounces and complaints
  • Creates an SNS topic (ses-events-prod / ses-events-dev) and email subscription
  • Wires the configuration set to publish BOUNCE, COMPLAINT, DELIVERY_DELAY events to SNS
  • Creates two CloudWatch alarms:
    • Bounce rate > 5% (AWS auto-suspends at 10%)
    • Complaint rate > 0.1% (AWS auto-suspends at 0.5%)

Note on CloudWatch provider: AWS publishes SES reputation metrics to us-east-1 only, regardless of sending region. The alarms use provider = aws.us_east_1. Prod already has this alias. Staging needs it added — see starlightpractice-dev-main-addition.tf.


Deployment Order

Step 1 → tofu apply (starlightpractice-prod)
Step 2 → tofu apply (starlightpractice-dev)
Step 3 → Poll DKIM status until "SUCCESS" (5–30 min)
Step 4 → Submit sandbox removal via AWS Console (both accounts)
Step 5 → Once SST Lambda roles exist: tofu apply -var='lambda_role_name=...'
Step 6 → Dev team passes ConfigurationSetName in every SendEmail call
         prod: "starlight-prod" | staging: "starlight-dev"

Apply Commands

# Step 1 — Production (apply first: root domain lives here)
cd accounts/starlightpractice-prod
tofu init && tofu plan && tofu apply

# Step 2 — Staging
cd accounts/starlightpractice-dev
tofu init && tofu plan && tofu apply
# ============================================================================
# accounts/starlightpractice-dev/main.tf — ADDITION ONLY
# ============================================================================
# Add this provider block to the EXISTING main.tf. Do not replace the file.
#
# WHY: AWS publishes SES reputation metrics (used by CloudWatch alarms in
# Task 5) to us-east-1 only, regardless of the SES sending region. The
# alarm resources in ses.tf use `provider = aws.us_east_1` and require
# this alias. The prod account already has it — staging does not.
#
# Insert after the existing provider block in main.tf.
# ============================================================================
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
profile = "starlightpractice-dev"
}
# ============================================================================
# accounts/starlightpractice-dev/ses.tf
# ============================================================================
# Covers Tasks 1, 3, 4, 5 for staging (dev.starlightpractice.com)
#
# Prerequisites:
# - aws_route53_zone.main already exists in main.tf
# - Add us_east_1 provider alias to main.tf (see starlightpractice-dev-main-addition.tf)
# ============================================================================
# ============================================================================
# Task 1: Domain Verification — SES Identity + DKIM + MAIL FROM
# ============================================================================
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"
}
}
# Add the 3 DKIM CNAME records to DNS
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"]
}
# Enable MAIL FROM domain (improves deliverability via 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
# ============================================================================
variable "lambda_role_name" {
description = "Name of the Lambda execution role that needs SES send permissions. Leave blank to skip — populate once the SST app is deployed."
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: Monitoring — Configuration Set + SNS + CloudWatch Alarms
# ============================================================================
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"
}
}
# SNS topic for bounce/complaint notifications
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
}
# SES configuration set for tracking sends/bounces/complaints
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
}
}
}
# CloudWatch alarm if bounce rate exceeds 5%
# SES reputation metrics are in us-east-1 regardless of sending region
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
# ============================================================================
# Covers Tasks 1, 3, 4, 5 for production (starlightpractice.com)
#
# Prerequisites:
# - aws_route53_zone.root already exists in main.tf
# - provider "aws" { alias = "us_east_1" } already exists in main.tf
# ============================================================================
# ============================================================================
# Task 1: Domain Verification — SES Identity + DKIM + MAIL FROM
# ============================================================================
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"
}
}
# Add the 3 DKIM CNAME records to DNS
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"]
}
# Enable MAIL FROM domain (improves deliverability via 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
# ============================================================================
variable "lambda_role_name" {
description = "Name of the Lambda execution role that needs SES send permissions. Leave blank to skip — populate once the SST app is deployed."
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: Monitoring — Configuration Set + SNS + CloudWatch Alarms
# ============================================================================
resource "aws_sesv2_configuration_set" "main" {
configuration_set_name = "starlight-prod"
sending_options {
sending_enabled = true
}
suppression_options {
suppressed_reasons = ["BOUNCE", "COMPLAINT"]
}
tags = {
Environment = "prod"
Project = "starlightpractice"
ManagedBy = "OpenTofu"
}
}
# SNS topic for bounce/complaint notifications
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
}
# SES configuration set for tracking sends/bounces/complaints
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
}
}
}
# CloudWatch alarm if bounce rate exceeds 5%
# SES reputation metrics are in us-east-1 regardless of sending region
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"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment