Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Terraform deployment #140

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,39 @@ likely need to adjust the bucket policy statement with one like this:
8. Optionally set the S3 lifecycle for this bucket to delete/expire objects
after a few days to clean up the saved emails.

## Set up with Terraform

1. Clone the project and access the project directory
```bash
git clone https://github.com/arithmetric/aws-lambda-ses-forwarder.git
cd aws-lambda-ses-forwarder
```

2. Modify the values in the `config` object at the top of `index.js` to specify
the S3 bucket and object prefix for locating emails stored by SES. Also provide
the email forwarding mapping from original destinations to new destination.

Use `YOURDOMAIN-aws-lambda-ses-forwarder-bucket` and replace YOURDOMAIN as
the name for your bucket. For example: `example.com-aws-lambda-ses-forwarder-bucket`.

3. Zip index.js into a function.zip and move it to the terraform directory
```bash
zip function.zip index.js
mv function.zip ./terraform/
```

4. Go to the terraform project and run terraform init and apply
```bash
cd terraform
terraform init
terraform apply
```
Terraform apply will ask you for the domain name (with subdomains) and the base domain name.
If you are not using subdomains, you can use the same value. For example: `example.com`.

If you are using an email key prefix, you should run
`terraform apply -var="email_key_prefix=YOURPREFIX"` instead.

## Extending

By loading aws-lambda-ses-forwarder as a module in a Lambda script, you can
Expand Down
5 changes: 5 additions & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform
*.tfstate
*.tfstate.backup

function.zip
25 changes: 25 additions & 0 deletions terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions terraform/lambda.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Setup the Lambda function
resource "aws_iam_role" "forwarder_function" {
name = "ses-forwarder-function-role"

assume_role_policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : "sts:AssumeRole",
"Principal" : {
"Service" : "lambda.amazonaws.com"
},
"Effect" : "Allow",
"Sid" : ""
}
]
}
)
}
resource "aws_lambda_function" "forwarder" {
function_name = "ses-forwarder-function"

filename = "function.zip"
source_code_hash = filebase64sha256("function.zip")

role = aws_iam_role.forwarder_function.arn
runtime = "nodejs12.x"
handler = "index.handler"
}

// Setup Lambda function logging
resource "aws_cloudwatch_log_group" "forwarder_function" {
name = "/aws/lambda/${aws_lambda_function.forwarder.function_name}"
}
resource "aws_iam_policy" "forwarder_function_logs" {
name = "ses-forwarder-function-logging-policy"

policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource" : "${aws_cloudwatch_log_group.forwarder_function.arn}:*",
"Effect" : "Allow"
}
]
})
}
resource "aws_iam_role_policy_attachment" "forwarder_function_logging" {
role = aws_iam_role.forwarder_function.name
policy_arn = aws_iam_policy.forwarder_function_logs.arn
}
30 changes: 30 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.53.0"
}
}

# backend "s3" {
# bucket = ""
# region = ""
# dynamodb_table = ""
# encrypt = true

# key = ""
# }
}

provider "aws" {
default_tags {
tags = {
Project = "aws-lambda-ses-forwarder"
ManagedBy = "terraform"
}
}
}

locals {
aws_ses_receipt_rule_name = "${var.domain}-aws-lambda-ses-forwarder"
}
4 changes: 4 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "email_bucket_name" {
description = "Email bucket name"
value = aws_s3_bucket.email.id
}
72 changes: 72 additions & 0 deletions terraform/s3.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
data "aws_caller_identity" "current" {}

resource "aws_s3_bucket" "email" {
bucket = "${var.domain}-aws-lambda-ses-forwarder-bucket"

# Might have to uncomment this to be able to destroy the bucket
# force_destroy = true
}

resource "aws_s3_bucket_acl" "email" {
bucket = aws_s3_bucket.email.id
acl = "private"
}

resource "aws_s3_bucket_public_access_block" "email" {
bucket = aws_s3_bucket.email.id

block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

// Allow SES to put emails
resource "aws_s3_bucket_policy" "email" {
bucket = aws_s3_bucket.email.id

policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "AllowSESPuts",
"Effect" : "Allow",
"Principal" : {
"Service" : "ses.amazonaws.com"
},
"Action" : "s3:PutObject",
"Resource" : "${aws_s3_bucket.email.arn}/*",
"Condition" : {
"StringEquals" : {
"AWS:SourceAccount" : "${data.aws_caller_identity.current.account_id}",
"AWS:SourceArn" : "arn:aws:ses:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:receipt-rule-set/${data.aws_ses_active_receipt_rule_set.main.rule_set_name}:receipt-rule/${local.aws_ses_receipt_rule_name}"
#"AWS:SourceArn" : "${aws_ses_receipt_rule.store.arn}" # Can't use because of dependencies
}
}
}
]
})
}

// Allow Lambda to put and get emails
resource "aws_iam_policy" "forwarder_function_bucket" {
name = "ses-forwarder-function-bucket-policy"

policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"s3:GetObject",
"s3:PutObject"
],
"Resource" : "${aws_s3_bucket.email.arn}/*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "forwarder_function_bucket" {
role = aws_iam_role.forwarder_function.name
policy_arn = aws_iam_policy.forwarder_function_bucket.arn
}
117 changes: 117 additions & 0 deletions terraform/ses.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
data "aws_region" "current" {}

data "aws_route53_zone" "domain" {
name = var.base_domain
}

resource "aws_ses_domain_identity" "domain" {
domain = var.domain
}

resource "aws_ses_domain_dkim" "domain" {
domain = aws_ses_domain_identity.domain.domain
}

resource "aws_ses_domain_mail_from" "domain" {
domain = aws_ses_domain_identity.domain.domain
mail_from_domain = "bounce.${aws_ses_domain_identity.domain.domain}"
}

resource "aws_route53_record" "domain_dkim" {
count = 3
zone_id = data.aws_route53_zone.domain.id
name = "${aws_ses_domain_dkim.domain.dkim_tokens[count.index]}._domainkey.${aws_ses_domain_identity.domain.domain}"
type = "CNAME"
ttl = "600"
records = ["${aws_ses_domain_dkim.domain.dkim_tokens[count.index]}.dkim.amazonses.com"]

allow_overwrite = var.allow_route53_overwrite
}

resource "aws_route53_record" "domain_from_mx" {
zone_id = data.aws_route53_zone.domain.id
name = aws_ses_domain_mail_from.domain.mail_from_domain
type = "MX"
ttl = "600"
records = ["10 feedback-smtp.${data.aws_region.current.name}.amazonses.com"]

allow_overwrite = var.allow_route53_overwrite
}

resource "aws_route53_record" "domain_from_txt" {
zone_id = data.aws_route53_zone.domain.id
name = aws_ses_domain_mail_from.domain.mail_from_domain
type = "TXT"
ttl = "600"
records = ["v=spf1 include:amazonses.com -all"]

allow_overwrite = var.allow_route53_overwrite
}

# Email receiving https://docs.aws.amazon.com/ses/latest/dg/receiving-email-mx-record.html
resource "aws_route53_record" "domain_receiving" {
zone_id = data.aws_route53_zone.domain.id
name = aws_ses_domain_identity.domain.domain
type = "MX"
ttl = "600"
records = ["10 inbound-smtp.${data.aws_region.current.name}.amazonaws.com"]

allow_overwrite = var.allow_route53_overwrite
}

// Allow Lambda to send emails
resource "aws_iam_policy" "forwarder_function_send_email" {
name = "ses-forwarder-function-send-email-policy"

policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : "ses:SendRawEmail",
"Resource" : "${aws_ses_domain_identity.domain.arn}"
}
]
})
}
resource "aws_iam_role_policy_attachment" "forwarder_function_send_email" {
role = aws_iam_role.forwarder_function.name
policy_arn = aws_iam_policy.forwarder_function_send_email.arn
}


# Allow SES to invoke our Lambda forwarder function
resource "aws_lambda_permission" "ses_invoke" {
statement_id = "AllowSESInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.forwarder.function_name
principal = "ses.amazonaws.com"
source_arn = "arn:aws:ses:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:receipt-rule-set/${data.aws_ses_active_receipt_rule_set.main.rule_set_name}:receipt-rule/${local.aws_ses_receipt_rule_name}"
}

data "aws_ses_active_receipt_rule_set" "main" {}

resource "aws_ses_receipt_rule" "store" {
depends_on = [
aws_s3_bucket_policy.email,
aws_lambda_permission.ses_invoke
]

name = local.aws_ses_receipt_rule_name
rule_set_name = data.aws_ses_active_receipt_rule_set.main.rule_set_name

enabled = true
recipients = [var.domain]
scan_enabled = true

s3_action {
position = 1
bucket_name = aws_s3_bucket.email.id
object_key_prefix = var.email_key_prefix
}

lambda_action {
position = 2
function_arn = aws_lambda_function.forwarder.arn
}
}
27 changes: 27 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
variable "domain" {
description = "Domain name"
type = string
}

# This should be the name of the hosted zone, without subdomains
variable "base_domain" {
description = "Domain base name"
type = string
}

# TODO:
# If the SES identity is already verified through Route53 records, Terraform might try
# to overwrite those records with the same ones and if this is not set to true, it
# will cancel the deployment.
# Terraform should check if the records are already created instead.
variable "allow_route53_overwrite" {
description = "Allow overwriting Route53 SES records"
type = bool
default = false
}

variable "email_key_prefix" {
description = "Email key prefix"
type = string
default = ""
}