Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cloudwatch/metric/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ locals {
var.color,
]))

id = "m_${var.name}_${local.vars_hash}"
id = "m_${replace(var.name, ".", "_")}_${local.vars_hash}"
}
4 changes: 2 additions & 2 deletions cloudwatch/metric/many/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module "default" {

locals {
out = [for v in var.vars : {
id = "m_${v.name}_${md5(jsonencode([
id = "m_${replace(v.name, ".", "_")}_${md5(jsonencode([
v.namespace,
v.name,
try(v.dimensions, module.default.dimensions),
Expand All @@ -27,7 +27,7 @@ locals {
}]

out_map = { for k, v in var.vars_map : k => {
id = "m_${v.name}_${md5(jsonencode([
id = "m_${replace(v.name, ".", "_")}_${md5(jsonencode([
v.namespace,
v.name,
try(v.dimensions, module.default.dimensions),
Expand Down
16 changes: 16 additions & 0 deletions ses/domain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ Registers a domain with AWS SES and verifies it

## Outputs

* `configuration_set`

Configuration set to use to track metrics for this domain

* `email_headers`

Headers that should be included in each email

* `metrics`

Cloudwatch metrics, see [metrics.tf](./metrics.tf) for details

* `sender_policy_arn`

IAM policy ARN for email senders
Expand All @@ -111,3 +123,7 @@ Registers a domain with AWS SES and verifies it
* `spf_record`

SPF record which you should include in the domain's TXT record in case you specified `spf = false`

* `widgets`

Cloudwatch dashboard widgets, see [widgets.tf](./widgets.tf) for details
25 changes: 25 additions & 0 deletions ses/domain/example/bin/spammer
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh

set -e

image_id=$(
docker build -q -f - bin <<EOF
FROM python:3.7-alpine

RUN pip install boto3
ADD ./spammer.py /opt/spammer/spammer.py
ENTRYPOINT ["python", "/opt/spammer/spammer.py"]
EOF
)

docker run \
-t \
--rm \
-e AWS_DEFAULT_REGION="eu-west-1" \
-e AWS_PROFILE="$AWS_PROFILE" \
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
-e SPAMMER_DEFAULT_SENDER="no-reply@$(terraform output domain)" \
-e SPAMMER_DEFAULT_CONFIGURATION_SET="$(terraform output configuration_set)" \
-v "$HOME/.aws:/root/.aws:ro" \
"$image_id" "$@"
148 changes: 148 additions & 0 deletions ses/domain/example/bin/spammer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import argparse
import os
import random
import sys
import time
from itertools import count
from textwrap import dedent
from email.message import EmailMessage

import boto3

EICAR_TEST_FILE = rb"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"

parser = argparse.ArgumentParser(
description="""
Simple script to generate some SES traffic
for example cloudwatch dashboards
""",
)
parser.add_argument(
"--success",
type=int,
default=5,
help="Successful delivery weight",
)
parser.add_argument(
"--success-email",
default="success@simulator.amazonses.com",
help="Where to send emails which should succeed",
)
parser.add_argument(
"--bounce",
type=int,
default=2,
help="Hard bounce weight",
)
parser.add_argument(
"--complaint",
type=int,
default=1,
help="Complaint weight",
)
parser.add_argument(
"--reject",
type=int,
default=1,
help="Anti-virus rejection weight",
)
parser.add_argument(
"--reject-email",
help="Verified email address that should be the recipient of reject emails",
)
parser.add_argument(
"--interval",
type=int,
default=5,
help="delay in seconds between each email",
)
parser.add_argument(
"--sender",
default=os.environ["SPAMMER_DEFAULT_SENDER"],
help="sender email address",
)
parser.add_argument(
"--configuration-set",
default=os.environ["SPAMMER_DEFAULT_CONFIGURATION_SET"],
help="configuration set to use",
)

def build_email_message(i, args):
email_type = random.choices(
population=["success", "bounce", "complaint", "reject"],
weights=[args.success, args.bounce, args.complaint, args.reject],
k=1,
)[0]

email_message = EmailMessage()
email_message["subject"] = f"{email_type} email"
email_message["from"] = args.sender
email_message["to"] = getattr(
args,
f"{email_type}_email",
f"{email_type}@simulator.amazonses.com",
)
email_message["X-SES-CONFIGURATION-SET"] = args.configuration_set

email_message.set_content(
dedent(
f"""
{email_type} email {i}
https://github.com/codequest-eu/terraform-modules
"""
),
subtype="plain",
)

email_message.add_alternative(
dedent(
f"""
<!DOCTYPE html>
<html>
<body>
<p>{email_type} email {i}</p>
<p><a href="https://github.com/codequest-eu/terraform-modules">codequest-eu/terraform-modules</a></p>
</body>
</html>
"""
),
subtype="html",
)

if email_type == "reject":
email_message.add_attachment(
EICAR_TEST_FILE,
filename="sample.txt",
maintype="application",
subtype="octet-stream",
)

return email_message

def main():
args = parser.parse_args()

if args.reject and not args.reject_email:
print("You have to specify --reject-email or disable reject emails with --reject 0")
sys.exit(1)

ses = boto3.client("ses")

print(f"Will start sending emails from {args.sender} every {args.interval} seconds")

for i in count(start=1):
time.sleep(args.interval)

email_message = build_email_message(i, args)
subject = email_message["subject"]
recipient = email_message["to"]

print(f"Sending {subject} to {recipient}...")
message_id = ses.send_raw_email(
RawMessage=dict(Data=email_message.as_bytes()),
)["MessageId"]
print(f"Sent {message_id}")


if __name__ == "__main__":
main()
22 changes: 22 additions & 0 deletions ses/domain/example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ module "domain" {
hosted_zone_id = data.aws_route53_zone.zone.id
}

output "domain" {
value = "ses-domain.terraform-modules-examples.${var.zone_domain}"
}

output "configuration_set" {
value = module.domain.configuration_set
}

output "sender_policy_arn" {
value = module.domain.sender_policy_arn
}

module "dashboard" {
source = "./../../../cloudwatch/dashboard"

name = "terraform-modules-ses-domain-example"
widgets = [
module.domain.widgets.delivery,
module.domain.widgets.delivery_percentage,
module.domain.widgets.spam,
module.domain.widgets.conversion,
module.domain.widgets.account_bounce_rate,
module.domain.widgets.account_spam_rate,
]
}
40 changes: 39 additions & 1 deletion ses/domain/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ resource "aws_route53_record" "dmarc" {
records = [join(";", [for k, v in local.dmarc_options : "${k}=${v}"])]
}

# sender policy
# sender policy ---------------------------------------------------------------

data "aws_iam_policy_document" "sender" {
count = var.create ? 1 : 0
Expand Down Expand Up @@ -150,3 +150,41 @@ resource "aws_iam_policy" "sender" {
description = "Allows sending emails from @${local.domain}"
policy = data.aws_iam_policy_document.sender[0].json
}

# configuration set for cloudwatch metrics ------------------------------------

resource "aws_ses_configuration_set" "domain" {
count = var.create ? 1 : 0

name = replace(local.domain, ".", "-")
}

locals {
configuration_set_name = var.create ? aws_ses_configuration_set.domain[0].name : ""
}

resource "aws_ses_event_destination" "metrics" {
count = var.create ? 1 : 0

configuration_set_name = local.configuration_set_name
name = "metrics"
enabled = true

# track all types
matching_types = [
"send",
"reject",
"bounce",
"complaint",
"delivery",
"open",
"click",
"renderingFailure"
]

cloudwatch_destination {
value_source = "messageTag"
dimension_name = "ses:from-domain"
default_value = local.domain
}
}
85 changes: 85 additions & 0 deletions ses/domain/metrics.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
locals {
metrics = {
emails = module.metrics_count.out_map.sent
rejections = module.metrics_count.out_map.rejections
bounces = module.metrics_count.out_map.bounces
spam = module.metrics_count.out_map.spam
deliveries = module.metrics_count.out_map.deliveries
opens = module.metrics_count.out_map.opens
clicks = module.metrics_count.out_map.clicks

rejections_percentage = module.metrics_percentage.out_map.rejections
bounces_percentage = module.metrics_percentage.out_map.bounces
deliveries_percentage = module.metrics_percentage.out_map.deliveries

account_bounce_rate = module.metrics_account_reputation.out_map.bounce
account_spam_rate = module.metrics_account_reputation.out_map.spam
}
}

module "cloudwatch_consts" {
source = "./../../cloudwatch/consts"
}

locals {
colors = module.cloudwatch_consts.colors
}

locals {
domain_dimensions = {
"ses:from-domain" = local.domain
}

metrics_count = {
sent = { name = "Send", label = "Sent", color = local.colors.grey }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too few colors ;) I understand that this is not a problem - they will not be presented in the same widget?

Copy link
Collaborator Author

@mskrajnowski mskrajnowski May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using grey to signify the desired/expected values, in this case sent is the desired value for the delivered metric, so that's the logic behind making sent grey

rejections = { name = "Reject", label = "Rejections", color = local.colors.red }
bounces = { name = "Bounce", label = "Bounces", color = local.colors.orange }
spam = { name = "Complaint", label = "Marked as spam", color = local.colors.red }
deliveries = { name = "Delivery", label = "Deliveries", color = local.colors.green }
opens = { name = "Open", label = "Opens", color = local.colors.light_green }
clicks = { name = "Click", label = "Link clicks", color = local.colors.green }
}
}

module "metrics_count" {
source = "./../../cloudwatch/metric/many"

vars_map = { for k, v in local.metrics_count : k => {
namespace = "AWS/SES"
dimensions = local.domain_dimensions
name = v.name
label = v.label
color = v.color
stat = "SampleCount"
period = 300
} }
}

module "metrics_percentage" {
source = "./../../cloudwatch/metric_expression/many"

vars_map = { for k, v in local.metrics_count : k => {
expression = "100 * ${module.metrics_count.out_map[k].id} / FILL(${module.metrics_count.out_map.sent.id}, 1)"
label = v.label
color = v.color
} }
}

locals {
metrics_reputation = {
bounce = { name = "Bounce", label = "Bounce" }
spam = { name = "Complaint", label = "Spam" }
}
}

module "metrics_account_reputation" {
source = "./../../cloudwatch/metric/many"

vars_map = { for k, v in local.metrics_reputation : k => {
namespace = "AWS/SES"
name = "Reputation.${v.name}Rate"
label = "${v.label} rate"
stat = "Average"
period = 3600
} }
}
Loading