Skip to content

Instantly share code, notes, and snippets.

@carlgieringer
Last active August 6, 2020 03:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlgieringer/b4ccca1e5f1c6a7b91cc4215b78595f2 to your computer and use it in GitHub Desktop.
Save carlgieringer/b4ccca1e5f1c6a7b91cc4215b78595f2 to your computer and use it in GitHub Desktop.
AWS SES via VPC Lambda
# Use like:
# variable "ses_lambda_package_path" {
# description = "The path to the Lambda package for the ses_lambda module"
# }
# module "ses_lambda" {
# source = "./modules/ses_lambda"
# lambda_package_path = var.ses_lambda_package_path
# }
# Creates a VPC containing for running a Lambda against an SES Endpoint
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-set-up-vpc-endpoints.html
# https://aws.amazon.com/blogs/aws/new-amazon-simple-email-service-ses-for-vpc-endpoints/
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html
variable "lambda_package_path" {}
locals {
subnet_count = length(data.aws_availability_zones.non_local.names)
cidr_block = "10.0.0.0/16"
region = "us-east-1"
smtp_ports = [25, 465, 587, 2465, 2587]
}
resource "aws_vpc" "ses_test" {
cidr_block = local.cidr_block
enable_dns_hostnames = true
tags = {
Name = "ses_test"
Terraform = "true"
}
}
resource "aws_subnet" "ses_test" {
count = local.subnet_count
vpc_id = aws_vpc.ses_test.id
cidr_block = cidrsubnet(local.cidr_block, ceil(log(local.subnet_count, 2)), count.index)
availability_zone = data.aws_availability_zones.non_local.names[count.index]
tags = {
Name = "ses_test"
Terraform = "true"
}
}
data "aws_availability_zones" "non_local" {
# No Local Zones
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones#by-filter
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
resource "aws_security_group" "allow_smtp" {
name = "allow_smtp"
description = "Allow SMTP inbound traffic"
vpc_id = aws_vpc.ses_test.id
tags = {
Terraform = "true"
}
}
resource "aws_security_group_rule" "allow_smtp" {
count = length(local.smtp_ports)
security_group_id = aws_security_group.allow_smtp.id
type = "ingress"
from_port = local.smtp_ports[count.index]
to_port = local.smtp_ports[count.index]
protocol = "tcp"
// source_security_group_id = aws_security_group.ses_client.id
cidr_blocks = [local.cidr_block]
}
resource "aws_vpc_endpoint" "ses" {
vpc_id = aws_vpc.ses_test.id
service_name = "com.amazonaws.${local.region}.email-smtp"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.allow_smtp.id]
private_dns_enabled = true
subnet_ids = data.aws_subnet_ids.ses.ids
tags = {
Name = "Test SES Endpoint"
Terraform = "true"
}
}
data "aws_vpc_endpoint_service" "ses" {
service = "email-smtp"
}
data "aws_subnet_ids" "ses" {
vpc_id = aws_vpc.ses_test.id
filter {
name = "subnet-id"
values = aws_subnet.ses_test.*.id
}
filter {
name = "availability-zone"
values = data.aws_vpc_endpoint_service.ses.availability_zones
}
}
resource "aws_iam_role" "ses_test_lambda" {
name = "ses_test"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
tags = {
Terraform = "true"
}
}
resource "aws_iam_policy" "ses_send_email" {
name = "ses_send_email"
description = "Allows the role to send email via SES"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "lambda_ses_send_email" {
role = aws_iam_role.ses_test_lambda.name
policy_arn = aws_iam_policy.ses_send_email.arn
}
resource "aws_iam_role_policy_attachment" "lambda_vpc_execution" {
role = aws_iam_role.ses_test_lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
resource "aws_security_group" "ses_client" {
name = "ses_client"
description = "Identifies resources that can access SES"
vpc_id = aws_vpc.ses_test.id
tags = {
Name = "ses_client"
Terraform = "true"
}
}
resource "aws_lambda_function" "ses_test" {
function_name = "ses_test"
filename = var.lambda_package_path
role = aws_iam_role.ses_test_lambda.arn
handler = "sesTest.handler"
source_code_hash = filebase64sha256(var.lambda_package_path)
vpc_config {
subnet_ids = aws_vpc_endpoint.ses.subnet_ids
security_group_ids = [
aws_security_group.ses_client.id
]
}
runtime = "nodejs12.x"
environment {
variables = {
aws_region = local.region
// ses_endpoint = "email-smtp.us-east-1.amazonaws.com"
ses_endpoint = lookup(aws_vpc_endpoint.ses.dns_entry[0], "dns_name")
}
}
tags = {
Terraform = "true"
}
}
'use strict';
const aws = require('aws-sdk');
exports.handler = async (event, context, callback) => {
const region = process.env.aws_region;
const endpoint = process.env.ses_endpoint;
const do_use_sesv2 = process.env.sesv2;
console.log({region, endpoint, do_use_sesv2});
const sslEnabled = true;
const ses = new aws.SES({region, endpoint, sslEnabled});
const sesv2 = new aws.SESV2({region, endpoint, sslEnabled});
const v1Params = {
Source: "notifications@verified-site.com",
Destination: {ToAddresses: ["carl.gieinger@gmail.com"]},
Message: {
Subject: {Data: "Test Email"},
Body: {Text: {Data: "Test"}},
},
Tags: [{"Name": "MyTag", "Value": "MyTagValue"}],
};
const v2Params = {
FromEmailAddress: "notifications@verified-site.com",
Destination: {ToAddresses: ["carl.gieinger@gmail.com"]},
Content: {
Simple: {
Subject: {Data: "Test Email"},
Body: {Text: {Data: "Test"}},
},
},
EmailTags: [{"Name": "MyTag", "Value": "MyTagValue"}],
};
try {
const response = await (do_use_sesv2 ?
sesv2.sendEmail(v2Params).promise() :
ses.sendEmail(v1Params).promise());
return context.succeed({response});
} catch (err) {
return context.fail(JSON.stringify(err));
}
};
# terraform show | python -c "import sys; sep = '\n\n'; sections = sys.stdin.read().split(sep); sections = [s for s in sections if 'module.ses_lambda' in s]; print(sep.join(sections));"
# module.ses_lambda.aws_iam_policy.ses_send_email:
resource "aws_iam_policy" "ses_send_email" {
arn = "arn:aws:iam::XXXXXXXXXXXX:policy/ses_send_email"
description = "Allows the role to send email via SES"
id = "arn:aws:iam::XXXXXXXXXXXX:policy/ses_send_email"
name = "ses_send_email"
path = "/"
policy = jsonencode(
{
Statement = [
{
Action = [
"ses:SendEmail",
"ses:SendRawEmail",
]
Effect = "Allow"
Resource = "*"
},
]
Version = "2012-10-17"
}
)
}
# module.ses_lambda.aws_iam_role.ses_test_lambda:
resource "aws_iam_role" "ses_test_lambda" {
arn = "arn:aws:iam::XXXXXXXXXXXX:role/ses_test"
assume_role_policy = jsonencode(
{
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Sid = ""
},
]
Version = "2012-10-17"
}
)
create_date = "2020-08-01T14:16:14Z"
force_detach_policies = false
id = "ses_test"
max_session_duration = 3600
name = "ses_test"
path = "/"
tags = {
"Terraform" = "true"
}
unique_id = "AROAQDVWX5AJVWFMG44DA"
}
# module.ses_lambda.aws_iam_role_policy_attachment.lambda_ses_send_email:
resource "aws_iam_role_policy_attachment" "lambda_ses_send_email" {
id = "ses_test-20200801141615369500000002"
policy_arn = "arn:aws:iam::XXXXXXXXXXXX:policy/ses_send_email"
role = "ses_test"
}
# module.ses_lambda.aws_iam_role_policy_attachment.lambda_vpc_execution:
resource "aws_iam_role_policy_attachment" "lambda_vpc_execution" {
id = "ses_test-20200801143344675200000001"
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
role = "ses_test"
}
# module.ses_lambda.aws_lambda_function.ses_test:
resource "aws_lambda_function" "ses_test" {
arn = "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:ses_test"
filename = "sesTest.zip"
function_name = "ses_test"
handler = "sesTest.handler"
id = "ses_test"
invoke_arn = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:ses_test/invocations"
last_modified = "2020-08-03T00:03:20.000+0000"
layers = []
memory_size = 128
publish = false
qualified_arn = "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:ses_test:$LATEST"
reserved_concurrent_executions = -1
role = "arn:aws:iam::XXXXXXXXXXXX:role/ses_test"
runtime = "nodejs12.x"
source_code_hash = "zFTNO2Dhsc98Uk5YOZ6KfwKqOpDg/j8mDJ/u8ozuujs="
source_code_size = 742
tags = {
"Terraform" = "true"
}
timeout = 3
version = "$LATEST"
# module.ses_lambda.aws_security_group.allow_smtp:
resource "aws_security_group" "allow_smtp" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:security-group/sg-039a6036662629ec7"
description = "Allow SMTP inbound traffic"
egress = []
id = "sg-039a6036662629ec7"
ingress = [
{
cidr_blocks = [
"10.0.0.0/16",
]
description = ""
from_port = 465
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "tcp"
security_groups = []
self = false
to_port = 465
},
{
cidr_blocks = [
"10.0.0.0/16",
]
description = ""
from_port = 587
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "tcp"
security_groups = []
self = false
to_port = 587
},
]
name = "allow_smtp"
owner_id = "XXXXXXXXXXXX"
revoke_rules_on_delete = false
tags = {
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_security_group.ses_client:
resource "aws_security_group" "ses_client" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:security-group/sg-03209f854ee71bf71"
description = "Identifies resources that can access SES"
egress = []
id = "sg-03209f854ee71bf71"
ingress = []
name = "ses_client"
owner_id = "XXXXXXXXXXXX"
revoke_rules_on_delete = false
tags = {
"Name" = "ses_client"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_security_group_rule.allow_smtp[4]:
resource "aws_security_group_rule" "allow_smtp" {
cidr_blocks = [
"10.0.0.0/16",
]
from_port = 2587
id = "sgrule-1330127352"
protocol = "tcp"
security_group_id = "sg-039a6036662629ec7"
self = false
to_port = 2587
type = "ingress"
}
# module.ses_lambda.aws_security_group_rule.allow_smtp[0]:
resource "aws_security_group_rule" "allow_smtp" {
cidr_blocks = [
"10.0.0.0/16",
]
from_port = 25
id = "sgrule-2704570716"
protocol = "tcp"
security_group_id = "sg-039a6036662629ec7"
self = false
to_port = 25
type = "ingress"
}
# module.ses_lambda.aws_security_group_rule.allow_smtp[1]:
resource "aws_security_group_rule" "allow_smtp" {
cidr_blocks = [
"10.0.0.0/16",
]
from_port = 465
id = "sgrule-3746546997"
protocol = "tcp"
security_group_id = "sg-039a6036662629ec7"
self = false
to_port = 465
type = "ingress"
}
# module.ses_lambda.aws_security_group_rule.allow_smtp[2]:
resource "aws_security_group_rule" "allow_smtp" {
cidr_blocks = [
"10.0.0.0/16",
]
from_port = 587
id = "sgrule-1830752222"
protocol = "tcp"
security_group_id = "sg-039a6036662629ec7"
self = false
to_port = 587
type = "ingress"
}
# module.ses_lambda.aws_security_group_rule.allow_smtp[3]:
resource "aws_security_group_rule" "allow_smtp" {
cidr_blocks = [
"10.0.0.0/16",
]
from_port = 2465
id = "sgrule-2752151293"
protocol = "tcp"
security_group_id = "sg-039a6036662629ec7"
self = false
to_port = 2465
type = "ingress"
}
# module.ses_lambda.aws_subnet.ses_test[2]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-0a1a5d9d3343cd948"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1c"
availability_zone_id = "use1-az4"
cidr_block = "10.0.64.0/19"
id = "subnet-0a1a5d9d3343cd948"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_subnet.ses_test[3]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-0b941e1f873105e86"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1d"
availability_zone_id = "use1-az6"
cidr_block = "10.0.96.0/19"
id = "subnet-0b941e1f873105e86"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_subnet.ses_test[4]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-0237e1ca03c42d6d2"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1e"
availability_zone_id = "use1-az3"
cidr_block = "10.0.128.0/19"
id = "subnet-0237e1ca03c42d6d2"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_subnet.ses_test[5]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-03c9528499f9a2ed8"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1f"
availability_zone_id = "use1-az5"
cidr_block = "10.0.160.0/19"
id = "subnet-03c9528499f9a2ed8"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_subnet.ses_test[0]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-0c1d0adc4e92e0d96"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1a"
availability_zone_id = "use1-az1"
cidr_block = "10.0.0.0/19"
id = "subnet-0c1d0adc4e92e0d96"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_subnet.ses_test[1]:
resource "aws_subnet" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:subnet/subnet-042d99b4f8d8ca044"
assign_ipv6_address_on_creation = false
availability_zone = "us-east-1b"
availability_zone_id = "use1-az2"
cidr_block = "10.0.32.0/19"
id = "subnet-042d99b4f8d8ca044"
map_public_ip_on_launch = false
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.aws_vpc.ses_test:
resource "aws_vpc" "ses_test" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:vpc/vpc-0f59a3c9e19e8304c"
assign_generated_ipv6_cidr_block = false
cidr_block = "10.0.0.0/16"
default_network_acl_id = "acl-0527a482dad02f22f"
default_route_table_id = "rtb-0d393f53695057bc7"
default_security_group_id = "sg-07d45cfa10d41fa17"
dhcp_options_id = "dopt-4bf2df2c"
enable_classiclink = false
enable_classiclink_dns_support = false
enable_dns_hostnames = true
enable_dns_support = true
id = "vpc-0f59a3c9e19e8304c"
instance_tenancy = "default"
main_route_table_id = "rtb-0d393f53695057bc7"
owner_id = "XXXXXXXXXXXX"
tags = {
"Name" = "ses_test"
"Terraform" = "true"
}
}
# module.ses_lambda.aws_vpc_endpoint.ses:
resource "aws_vpc_endpoint" "ses" {
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:vpc-endpoint/vpce-011dbce6e564c73f0"
cidr_blocks = []
dns_entry = [
{
dns_name = "vpce-011dbce6e564c73f0-xefabho1.email-smtp.us-east-1.vpce.amazonaws.com"
hosted_zone_id = "Z7HUB22UULQXV"
},
{
dns_name = "vpce-011dbce6e564c73f0-xefabho1-us-east-1a.email-smtp.us-east-1.vpce.amazonaws.com"
hosted_zone_id = "Z7HUB22UULQXV"
},
{
dns_name = "vpce-011dbce6e564c73f0-xefabho1-us-east-1d.email-smtp.us-east-1.vpce.amazonaws.com"
hosted_zone_id = "Z7HUB22UULQXV"
},
{
dns_name = "vpce-011dbce6e564c73f0-xefabho1-us-east-1c.email-smtp.us-east-1.vpce.amazonaws.com"
hosted_zone_id = "Z7HUB22UULQXV"
},
{
dns_name = "email-smtp.us-east-1.amazonaws.com"
hosted_zone_id = "Z0297579310RU7X0GWQOP"
},
]
id = "vpce-011dbce6e564c73f0"
network_interface_ids = [
"eni-045134ed552f8a94c",
"eni-05e09bd0f5f9ff5c9",
"eni-0ff5f874bf3e3f619",
]
owner_id = "XXXXXXXXXXXX"
policy = jsonencode(
{
Statement = [
{
Action = "*"
Effect = "Allow"
Principal = "*"
Resource = "*"
},
]
}
)
private_dns_enabled = true
requester_managed = false
route_table_ids = []
security_group_ids = [
"sg-039a6036662629ec7",
]
service_name = "com.amazonaws.us-east-1.email-smtp"
state = "available"
subnet_ids = [
"subnet-0a1a5d9d3343cd948",
"subnet-0b941e1f873105e86",
"subnet-0c1d0adc4e92e0d96",
]
tags = {
"Name" = "Test SES Endpoint"
"Terraform" = "true"
}
vpc_endpoint_type = "Interface"
vpc_id = "vpc-0f59a3c9e19e8304c"
}
# module.ses_lambda.data.aws_availability_zones.non_local:
data "aws_availability_zones" "non_local" {
group_names = [
"us-east-1",
]
id = "2020-08-03 00:02:10.488538 +0000 UTC"
names = [
"us-east-1a",
"us-east-1b",
"us-east-1c",
"us-east-1d",
"us-east-1e",
"us-east-1f",
]
zone_ids = [
"use1-az1",
"use1-az2",
"use1-az4",
"use1-az6",
"use1-az3",
"use1-az5",
]
# module.ses_lambda.data.aws_subnet_ids.ses:
data "aws_subnet_ids" "ses" {
id = "vpc-0f59a3c9e19e8304c"
ids = [
"subnet-0a1a5d9d3343cd948",
"subnet-0b941e1f873105e86",
"subnet-0c1d0adc4e92e0d96",
]
vpc_id = "vpc-0f59a3c9e19e8304c"
# module.ses_lambda.data.aws_vpc_endpoint_service.ses:
data "aws_vpc_endpoint_service" "ses" {
acceptance_required = false
arn = "arn:aws:ec2:us-east-1:XXXXXXXXXXXX:vpc-endpoint-service/vpce-svc-0dfadb6990904aa18"
availability_zones = [
"us-east-1a",
"us-east-1c",
"us-east-1d",
]
base_endpoint_dns_names = [
"email-smtp.us-east-1.vpce.amazonaws.com",
]
id = "662324440"
manages_vpc_endpoints = false
owner = "amazon"
private_dns_name = "email-smtp.us-east-1.amazonaws.com"
service = "email-smtp"
service_id = "vpce-svc-0dfadb6990904aa18"
service_name = "com.amazonaws.us-east-1.email-smtp"
service_type = "Interface"
tags = {}
vpc_endpoint_policy_supported = false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment