From AWS Console to GitOps for SES Templates
IaC has rough edges for SES templates: noisy diffs, heavy CI, no non-engineer access. A pragmatic comparison of Terraform, CDK, and control layers.
Most engineering teams move from "we click around in the AWS Console" to "we manage AWS through infrastructure as code" sometime in their first two years. Templates often come along for the ride, declared in Terraform or CDK alongside the rest of the AWS infrastructure.
This is mostly correct and mostly fine. But SES templates have specific characteristics that make them awkward citizens in an IaC stack. They change often. They contain content, not configuration. The people who want to change them are sometimes not engineers. The diffs they produce in plan output are large and uninteresting.
The result is that teams who do everything else well in IaC frequently develop a quiet anti-pattern around SES templates: an aws_ses_template resource defined somewhere, edited rarely, with copy changes still happening in the Console because going through the IaC workflow for a comma fix is too painful.
This post is a practical look at where IaC works for SES, where it doesn't, and what to do at the seams.
Terraform for SES: what works
The Terraform AWS provider supports both v1 and v2 SES template resources. The v2 resource is preferred:
resource "aws_sesv2_email_template" "welcome" {
email_template_name = "welcome"
email_template_content {
subject = "Welcome to ${var.product_name}"
html = file("${path.module}/templates/welcome.html")
text = file("${path.module}/templates/welcome.txt")
}
}
What this gives you:
- Templates are checked into version control alongside the rest of your infrastructure.
terraform planshows you what would change before it changes.terraform applyis the deploy event, and it's auditable through your existing IaC pipeline.- State is tracked: Terraform knows which templates it manages, and
terraform destroycleanly removes them.
For low-frequency template changes — security flows, billing flows that change quarterly — this is a reasonable place to land. The change ceremony matches the change frequency.
Terraform for SES: what doesn't
The friction shows up when templates change often or when the people changing them are not Terraform users.
Plan diffs are large and noisy. A two-word change to a marketing-tinged welcome email produces a Terraform plan that shows the entire new HTML body. Reviewing this in a PR is unpleasant. Most reviewers learn to skim past it, which defeats the point of review.
Copy changes feel heavy. A typo fix becomes: branch, edit, plan, PR, review, merge, apply. For engineers, this is the same workflow as everything else. For a marketer who wants to fix a string, it's a wall.
State drift is unpleasant. If somebody edits a template in the Console (which the drift post explains will happen), Terraform's next plan shows that drift as something to be reverted. The right move on a hotfix is to backfill it into Terraform; the wrong move is to apply the plan and overwrite the fix. Neither is automatic.
Templating engines aren't IaC-friendly. SES templates use Handlebars-style variables. Some teams want to pre-process templates further (e.g., using a build step to inline shared partials). Doing that inside Terraform is awkward; doing it outside Terraform and feeding the result in works but adds steps.
Resource churn on update. The v1 aws_ses_template resource has known idiosyncrasies around updates. The v2 aws_sesv2_email_template is cleaner but still occasionally requires resource recreation for changes that should be in-place updates. Watch for these in plan output.
CDK and SAM
AWS CDK and SAM offer the same primitive with slightly different ergonomics. CDK gives you a programmatic interface in your language of choice:
new ses.CfnTemplate(this, 'WelcomeTemplate', {
template: {
templateName: 'welcome',
subjectPart: `Welcome to ${productName}`,
htmlPart: fs.readFileSync('templates/welcome.html', 'utf-8'),
textPart: fs.readFileSync('templates/welcome.txt', 'utf-8'),
},
});
CDK's strength: you can compose template management with the rest of your CDK app, share constants between templates and other resources, and write unit tests that verify rendered outputs. CDK's weakness: the deploy story is the same CloudFormation deploy story, with the same plan-and-apply ceremony as Terraform.
SAM is similar. CloudFormation under the hood. Same templates resource, same deploy ergonomics.
For teams that have committed to CDK or SAM elsewhere, using them for SES templates is consistent with the rest of the stack. The friction is the same as Terraform's: heavy ceremony for content changes, awkward for non-engineers.
Custom CI workflows around the SES API
The third path is to skip IaC entirely for templates and write a small CI workflow that calls the SES API directly:
name: Deploy SES templates
on:
push:
branches: [main]
paths: ["templates/**"]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.SES_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.SES_REGION }}
- run: ./scripts/deploy-templates.sh
The deploy script reads template files, calls UpdateEmailTemplate for each one, and reports results. Optionally, it pulls existing templates first to compute diffs and skip no-op updates.
What this gives you that IaC doesn't:
- Tighter control over the deploy. You can ramp publishes, send to seed lists, run pre-publish render checks — anything you can script.
- Cleaner state. The script doesn't track state — Git is the state. SES is the deploy target. There's no
terraform.tfstateto keep in sync. - Fewer surprises in plan output. A typo fix is just a typo fix in the diff.
What it costs:
- You're now maintaining a deploy script, which is software with bugs.
- IaC's "destroy" semantics are gone. Removing a template from Git doesn't delete it from SES unless you implement that explicitly.
- No state-tracked drift detection — you have to implement it separately.
For teams whose templates change frequently and whose engineers are comfortable maintaining small operational scripts, this is often the right path. It treats templates as content (versioned files) rather than infrastructure (declared resources).
Where IaC stops being the right surface
The pattern across all three IaC paths is the same: they're optimized for engineers, and they're expensive when content changes faster than infrastructure does.
The structural problem isn't IaC's quality. It's that IaC tools assume a few things that don't hold for transactional email templates:
That changes are reviewed by engineers. A welcome email's copy is not engineering content. The right reviewer is whoever owns the customer voice — marketing, lifecycle, support. Forcing those people into PR review of Terraform plans is a friction tax with no benefit.
That the deploy event is engineering-cadence. A typo gets noticed at 4pm Friday. The right cadence is "fixed by 4:30." IaC pipelines optimized for safety in production deploys are slower than that, deliberately.
That the resource is configuration. SES templates are mostly content with a thin layer of configuration. IaC tools are optimized for configuration changes. Content changes pass through, but the tooling doesn't help — content diff review, content rendering, content-specific testing are all out of scope.
When these assumptions don't fit, IaC isn't the wrong tool — it's just not the complete answer. You end up needing something else for the content layer, or accepting friction for the people who own the content.
Hybrid models: IaC for infrastructure, content layer for templates
The pattern that works well in larger teams: split the responsibilities cleanly.
IaC owns infrastructure. Domain identities, configuration sets, IAM roles, suppression lists at the account level, event destinations, IP pools. These are configuration. They change quarterly. IaC's review and deploy ceremony is appropriate. Terraform or CDK is the right tool.
A content layer owns templates. A separate system manages template content. It pushes templates to SES through the IaC-managed configuration sets. It can be a CI script, a custom internal tool, or a managed product like Sovy. The point is that template content is not a Terraform resource.
This split lets IaC do what IaC is good at (declarative infrastructure) while letting the content layer do what IaC isn't good at (frequent content changes by non-engineers, content-specific tooling, tight review loops).
The architectural diagram looks like:
- Terraform/CDK: identities, config sets, IAM, account-level settings.
- Content layer: templates, drafts, approvals, version history, audit.
- Application: sends through SES, referencing templates by name and configuration sets created by the IaC layer.
This is the architecture I see at companies that have been operating SES at scale for several years. They didn't start there. They got there by hitting the friction of pure IaC and looking for the seam.
A trade-offs cheat sheet
| Concern | Terraform / CDK | Custom CI script | Dedicated control layer |
|---|---|---|---|
| Engineers comfortable | Yes | Yes | Yes |
| Non-engineers can edit | No | No | Yes (designed for it) |
| Plan/diff visibility | Heavy | Light | Native |
| Deploy speed for typo fix | Slow | Medium | Fast |
| State tracking | Native | DIY | Native |
| Drift detection | Native (plan) | DIY | Native |
| Template-specific tooling | None | DIY | Native |
| Review surface for content | PR (text only) | PR (text only) | Visual |
| Audit beyond CloudTrail | Commit history | DIY | Native |
| Multi-region replication | Manual | DIY | Native |
| Operational complexity | Medium | Medium | Low (offloaded) |
There is no universally right column. Pick based on:
- How often templates change (rarely → IaC; often → content layer).
- Who needs to change them (engineers only → IaC works; non-engineers in the loop → content layer).
- Whether you have IaC discipline elsewhere (yes → IaC for infra is consistent; no → don't introduce it just for templates).
- Whether your team has bandwidth for in-house operational tools (yes → custom CI; no → buy).
Where Sovy fits
Sovy is the dedicated control layer column in that table. It owns templates: drafts, versions, reviews, publishes, audit. It interacts with SES through a single narrowly-scoped IAM role that you manage in your IaC. It does not manage configuration sets, identities, or quotas — those stay in your Terraform or CDK stack where they belong.
The Sovy argument is the hybrid argument: IaC is good at the things IaC is good at, and a content layer is good at the things a content layer is good at. Sovy is one option for the content layer. The custom CI approach is another. Pure IaC for both is a third, with the trade-offs documented above.
A pragmatic next step
If you're starting fresh, lean toward the hybrid: IaC for infrastructure, something else for content. You don't have to know what the "something else" is on day one — start with a CI script, see if the friction stays low, and graduate to a managed control layer when the script becomes its own maintenance burden.
If you're already on pure IaC and feeling the friction: don't rip it out. The cost of refactoring is high and you've already paid the IaC setup cost. Instead, identify the specific frictions — slow deploys, non-engineer access, content review — and address them surgically. Often, adding a marketer-friendly UI on top of the existing IaC with a webhook to trigger plans is enough to relieve the worst of the friction without restructuring.
If you're still on the AWS Console for everything, start with IaC for the infrastructure and a CI script for the templates. That combination buys you 80% of the safety of a dedicated control layer at 20% of the cost. Upgrade later if the friction comes back.
The goal isn't to pick the tool that the most blog posts recommend. The goal is to match the tool to the actual change cadence and the actual editors. SES templates change content-frequency, not infrastructure-frequency. Your tooling should reflect that.
Sovy is a content layer for Amazon SES templates that pairs with your existing IaC. Configuration sets, identities, and IAM stay in Terraform or CDK. Templates, drafts, audit, and review live in Sovy. If you've felt the friction of managing template copy through plan/apply, we'd like to hear from you.