diff --git a/examples/ses/.terraform.lock.hcl b/examples/ses/.terraform.lock.hcl new file mode 100644 index 0000000..62397a5 --- /dev/null +++ b/examples/ses/.terraform.lock.hcl @@ -0,0 +1,21 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.70.0" + constraints = "~> 3.0" + hashes = [ + "h1:jn4ImGMZJ9rQdaVSbcCBqUqnhRSpyaM1DivqaNuP+eg=", + "zh:0af710e528e21b930899f0ac295b0ceef8ad7b623dd8f38e92c8ec4bc7af0321", + "zh:4cabcd4519c0aae474d91ae67a8e3a4a8c39c3945c289a9cf7c1409f64409abe", + "zh:58da1a436facb4e4f95cd2870d211ed7bcb8cf721a4a61970aa8da191665f2aa", + "zh:6465339475c1cd3c16a5c8fee61304dcad2c4a27740687d29c6cdc90d2e6423d", + "zh:7a821ed053c355d70ebe33185590953fa5c364c1f3d66fe3f9b4aba3961646b1", + "zh:7c3656cc9cc1739dcb298e7930c9a76ccfce738d2070841d7e6c62fbdae74eef", + "zh:9d9da9e3c60a0c977e156da8590f36a219ae91994bb3df5a1208de2ab3ceeba7", + "zh:a3138817c86bf3e4dca7fd3a92e099cd1bf1d45ee7c7cc9e9773ba04fc3b315a", + "zh:a8603044e935dfb3cb9319a46d26276162c6aea75e02c4827232f9c6029a3182", + "zh:aef9482332bf43d0b73317f5909dec9e95b983c67b10d72e75eacc7c4f37d084", + "zh:fc3f3cad84f2eebe566dd0b65904c934093007323b9b85e73d9dd4535ceeb29d", + ] +} diff --git a/examples/ses/README.md b/examples/ses/README.md new file mode 100644 index 0000000..f07799a --- /dev/null +++ b/examples/ses/README.md @@ -0,0 +1,38 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.14.5 | +| [aws](#requirement\_aws) | ~> 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 3.70.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ses](#module\_ses) | ../../modules/ses | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_access_key.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | +| [aws_iam_user.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_iam_user_policy_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | +| [aws_route53_zone.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | + +## Outputs + +| Name | Description | +|------|-------------| +| [dkim\_verification\_attrs](#output\_dkim\_verification\_attrs) | n/a | +| [domain\_identity\_verification\_attrs](#output\_domain\_identity\_verification\_attrs) | n/a | +| [user\_key\_id](#output\_user\_key\_id) | n/a | +| [user\_secret](#output\_user\_secret) | n/a | + \ No newline at end of file diff --git a/examples/ses/main.tf b/examples/ses/main.tf new file mode 100644 index 0000000..da85544 --- /dev/null +++ b/examples/ses/main.tf @@ -0,0 +1,30 @@ +locals { + domain_name = "exmaple.com" +} + +module "ses" { + source = "../../modules/ses" + domain_name = local.domain_name + name_prefix = "exmpale-com" + + zone_id = data.aws_route53_zone.this.zone_id + verify_dkim = true +} + +resource "aws_iam_user" "this" { + name = "example-com" +} + +resource "aws_iam_access_key" "this" { + user = aws_iam_user.this.name +} + +resource "aws_iam_user_policy_attachment" "this" { + user = aws_iam_user.this.name + policy_arn = module.ses.send_email_policy_arn +} + +data "aws_route53_zone" "this" { + name = local.domain_name + private_zone = false +} diff --git a/examples/ses/outputs.tf b/examples/ses/outputs.tf new file mode 100644 index 0000000..8c25c15 --- /dev/null +++ b/examples/ses/outputs.tf @@ -0,0 +1,16 @@ +output "user_key_id" { + value = aws_iam_access_key.this.id +} + +output "user_secret" { + value = aws_iam_access_key.this.ses_smtp_password_v4 + sensitive = true +} + +output "dkim_verification_attrs" { + value = module.ses.dkim_verification_attrs +} + +output "domain_identity_verification_attrs" { + value = module.ses.domain_identity_verification_attrs +} diff --git a/examples/ses/versions.tf b/examples/ses/versions.tf new file mode 100644 index 0000000..245ccce --- /dev/null +++ b/examples/ses/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 0.14.5" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + } + } +} + +provider "aws" { + region = "eu-central-1" +} + diff --git a/modules/ses/.terraform.lock.hcl b/modules/ses/.terraform.lock.hcl new file mode 100644 index 0000000..51c915f --- /dev/null +++ b/modules/ses/.terraform.lock.hcl @@ -0,0 +1,21 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.71.0" + hashes = [ + "h1:5+M8SPZlb3FxcmAX4RykKzNrTHkpjoP1UpHcenOXcxo=", + "h1:eo+lJtStpFZv11FT9BLbY+dwC9LM+9JfjXtX+U5nGJE=", + "zh:173134d8861a33ed60a48942ad2b96b9d06e85c506d7f927bead47a28f4ebdd2", + "zh:2996c8e96930f526f1761e99d14c0b18d83e287b1362aa2fa1444cf848ece613", + "zh:43903da1e0a809a1fb5832e957dbe2321b86630d6bfdd8b47728647a72fd912d", + "zh:43e71fd8924e7f7b56a0b2a82e29edf07c53c2b41ee7bb442a2f1c27e03e86ae", + "zh:4f4c73711f64a3ff85f88bf6b2594e5431d996b7a59041ff6cbc352f069fc122", + "zh:5045241b8695ffbd0730bdcd91393b10ffd0cfbeaad6254036e42ead6687d8fd", + "zh:6a8811a0fb1035c09aebf1f9b15295523a9a7a2627fd783f50c6168a82e192dd", + "zh:8d273c04d7a8c36d4366329adf041c480a0f1be10a7269269c88413300aebdb8", + "zh:b90505897ae4943a74de2b88b6a9e7d97bf6dc325a0222235996580edff28656", + "zh:ea5e422942ac6fc958229d27d4381c89d21d70c5c2c67a6c06ff357bcded76f6", + "zh:f1536d7ff2d3bfd668e3ac33d8956b4f988f87fdfdcc371c7d94b98d5dba53e2", + ] +} diff --git a/modules/ses/README.md b/modules/ses/README.md new file mode 100644 index 0000000..94effa5 --- /dev/null +++ b/modules/ses/README.md @@ -0,0 +1,42 @@ + + + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 3.71.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_route53_record.ses_dmarc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_route53_record.ses_spf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_route53_record.this_verify_dkim](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_ses_domain_dkim.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_domain_dkim) | resource | +| [aws_ses_domain_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_domain_identity) | resource | +| [aws_ses_email_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_email_identity) | resource | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [dmarc](#input\_dmarc) | DMARC record for domain. Read full spec: https://mxtoolbox.com/dmarc/details/what-is-a-dmarc-record
v : required, protocol version (v=DMARC1)
p : required, policy (p=none\|quarantine\|reject)
pct : optional, percentage of messages subjected to filtering (0-100)
rua : optional, reporting URI for aggregate reports (mailto:aaa@domain.tld,mailto:bbb@domain.tld)
ruf : optional, reporting URI for forensic reports (mailto:aaa@domain.tld,mailto:bbb@domain.tld)
fo : optional, failure reporting options (fo=0\|1\|d\|s)
aspf : optional, The aspf tag represents alignment mode for SPF. An optional tag, aspf=r is a common example of its configuration.
adkim : optional, The adkim tag represents alignment mode for DKIM. An optional tag, adkim=r is a common example of its configuration.
rf : optional, The rf tag represents reporting format. An optional tag, rf=afrf is a common example of its configuration.
ri : optional, The ri tag represents reporting interval. An optional tag, ri=86400 is a common example of its configuration.
sp : optional, The sp tag represents subdomain policy. An optional tag, sp=reject is a common example of its configuration. |
object({
v = string # required, protocol version (v=DMARC1)
p = string # required, policy (p=none|quarantine|reject)
pct = number # optional, percentage of messages subjected to filtering (0-100)
rua = string # optional, reporting URI for aggregate reports (mailto:aaa@domain.tld,mailto:bbb@domain.tld)
ruf = string # optional, reporting URI for forensic reports (mailto:aaa@domain.tld,mailto:bbb@domain.tld)
fo = string # optional, failure reporting options (fo=0|1|d|s)
aspf = string # optional, The aspf tag represents alignment mode for SPF. An optional tag, aspf=r is a common example of its configuration.
adkim = string # optional, The adkim tag represents alignment mode for DKIM. An optional tag, adkim=r is a common example of its configuration.
rf = string # optional, The rf tag represents reporting format. An optional tag, rf=afrf is a common example of its configuration.
ri = string # optional, The ri tag represents reporting interval. An optional tag, ri=86400 is a common example of its configuration.
sp = string # optional, The sp tag represents subdomain policy. An optional tag, sp=reject is a common example of its configuration.
})
|
{
"adkim": "s",
"aspf": "s",
"fo": null,
"p": "reject",
"pct": "100",
"rf": null,
"ri": null,
"rua": null,
"ruf": null,
"sp": null,
"v": "DMARC1"
}
| no | +| [dmarc\_enabled](#input\_dmarc\_enabled) | Set DMARC record in Route53. | `bool` | `false` | no | +| [domain\_name](#input\_domain\_name) | The domain name from which AWS SES will be able to send emails. | `string` | n/a | yes | +| [email\_addresses](#input\_email\_addresses) | Emails from which AWS SES will be able to send emails. | `set(string)` | `[]` | no | +| [name\_prefix](#input\_name\_prefix) | Prefix that will be prepended to resource names | `string` | n/a | yes | +| [spf\_enabled](#input\_spf\_enabled) | Set SPF record in Route53. | `bool` | `false` | no | +| [verify\_dkim](#input\_verify\_dkim) | Automatically verify DKIM records in Route53. | `bool` | `false` | no | +| [zone\_id](#input\_zone\_id) | The Route53 zone ID for the domain name. | `string` | `""` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [dkim\_verification\_attrs](#output\_dkim\_verification\_attrs) | DKIM name, value, type attributes needed to verify domain | +| [send\_email\_policy\_arn](#output\_send\_email\_policy\_arn) | IAM policy ARN for sending emails | + \ No newline at end of file diff --git a/modules/ses/main.tf b/modules/ses/main.tf new file mode 100644 index 0000000..aa62493 --- /dev/null +++ b/modules/ses/main.tf @@ -0,0 +1,87 @@ +locals { + dkim_verification_attrs = [for dkim in aws_ses_domain_dkim.this.dkim_tokens : { + name = "${dkim}._domainkey.${var.domain_name}" + ttl = 600 + type = "CNAME" + value = "${dkim}.dkim.amazonses.com" + }] + + # remove empty options + dmarc_record = join(";", compact([ + "v=${var.dmarc.v}", + "p=${var.dmarc.p}", + var.dmarc.pct == null ? null : "pct=${var.dmarc.pct}", + var.dmarc.rua == null ? null : "rua=${var.dmarc.rua}", + var.dmarc.ruf == null ? null : "ruf=${var.dmarc.ruf}", + var.dmarc.fo == null ? null : "fo=${var.dmarc.fo}", + var.dmarc.aspf == null ? null : "aspf=${var.dmarc.aspf}", + var.dmarc.adkim == null ? null : "adkim=${var.dmarc.adkim}", + var.dmarc.rf == null ? null : "rf=${var.dmarc.rf}", + var.dmarc.ri == null ? null : "ri=${var.dmarc.ri}", + var.dmarc.sp == null ? null : "sp=${var.dmarc.sp}", + ])) +} + +resource "aws_ses_email_identity" "this" { + for_each = var.email_addresses + email = each.key +} + +resource "aws_ses_domain_identity" "this" { + domain = var.domain_name +} + +resource "aws_ses_domain_dkim" "this" { + domain = aws_ses_domain_identity.this.domain +} + +data "aws_iam_policy_document" "this" { + statement { + actions = [ + "ses:SendEmail", + "ses:SendRawEmail", + ] + + resources = flatten( + [[for email in aws_ses_email_identity.this : email.arn], aws_ses_domain_identity.this.arn], + ) + } +} + +resource "aws_iam_policy" "this" { + name = "${var.name_prefix}-ses-send-email" + policy = data.aws_iam_policy_document.this.json +} + + +resource "aws_route53_record" "this_verify_dkim" { + count = var.zone_id != "" && var.verify_dkim ? 3 : 0 + + zone_id = var.zone_id + name = local.dkim_verification_attrs[count.index].name + type = local.dkim_verification_attrs[count.index].type + ttl = local.dkim_verification_attrs[count.index].ttl + records = [local.dkim_verification_attrs[count.index].value] +} + +resource "aws_route53_record" "ses_dmarc" { + count = var.zone_id != "" && var.dmarc_enabled ? 1 : 0 + + zone_id = var.zone_id + name = "_dmarc.${var.domain_name}" + type = "TXT" + ttl = 600 + records = [local.dmarc_record] +} + +resource "aws_route53_record" "ses_spf" { + count = var.zone_id != "" && var.spf_enabled ? 1 : 0 + + zone_id = var.zone_id + name = var.domain_name + type = "TXT" + ttl = 600 + records = [ + "v=spf1 include:amazonses.com -all" + ] +} diff --git a/modules/ses/outputs.tf b/modules/ses/outputs.tf new file mode 100644 index 0000000..751549c --- /dev/null +++ b/modules/ses/outputs.tf @@ -0,0 +1,9 @@ +output "dkim_verification_attrs" { + value = local.dkim_verification_attrs + description = "DKIM name, value, type attributes needed to verify domain" +} + +output "send_email_policy_arn" { + value = aws_iam_policy.this.arn + description = "IAM policy ARN for sending emails" +} diff --git a/modules/ses/variables.tf b/modules/ses/variables.tf new file mode 100644 index 0000000..2a2785e --- /dev/null +++ b/modules/ses/variables.tf @@ -0,0 +1,84 @@ +variable "name_prefix" { + type = string + description = "Prefix that will be prepended to resource names" +} + +variable "domain_name" { + type = string + description = "The domain name from which AWS SES will be able to send emails." +} + +# optional + +variable "email_addresses" { + type = set(string) + default = [] + description = "Emails from which AWS SES will be able to send emails." +} + +variable "zone_id" { + type = string + description = "The Route53 zone ID for the domain name." + default = "" +} + +variable "verify_dkim" { + type = bool + description = "Automatically verify DKIM records in Route53." + default = false +} + +variable "dmarc_enabled" { + type = bool + description = "Set DMARC record in Route53." + default = false +} + +variable "spf_enabled" { + type = bool + description = "Set SPF record in Route53." + default = false +} + +variable "dmarc" { + description = <