Package: aws-cfn-ses-domain
AWS CloudFormation resources for Amazon SES domain and email identitiesAWSCloudFormationprovides several built-inAmazon SES resource types, but is oddly missing the ones
most needed to get going with SES:domain and email verification.This package implements those missing types as CloudFormationcustom resources:Custom::SES_DomainandCustom::SES_EmailIdentity.You can useCustom::SES_Domainto verify domains for sending and/or receiving
email through Amazon SES. It handles the SES domain verification calls and outputs
SES's required DNS entries, so you can feed them directly into a standardAWS::Route53::RecordSetGroupresource (or to another DNS provider if you're not
using Route 53).Similarly, you can useCustom::SES_EmailIdentityto verify individual email addresses
for sending through Amazon SES.The custom resource implementations will properly create, update, and delete Amazon SES
identities as you modify your CloudFormation stack.DocumentationInstallationUsageCustom::SES_DomainPropertiesReturn ValuesCustom::SES_EmailIdentityPropertiesReturn ValuesValidating Your TemplatesDevelopmentAlternativesFutureInstallationThe custom SES resources are implemented as AWS Lambda Functions. To use them
from your CloudFormation templates, you'll need to set up the functions along with
IAM roles giving them permission to manage your Amazon SES domain/email identities.The easiest way to do this is with a CloudFormationnested stack:Copy theaws-cfn-ses-domain-VERSION.lambda.zipLambda package andaws-cfn-ses-domain-VERSION.cf.yamlCloudFormation template from this repository'sreleasespage into an S3 bucket in the region where you'll be running your
CloudFormation stack. This bucket needs to be readable from CloudFormation, but
need not be public.Then in your CloudFormation template, use a nested stack to create the Lambda
Functions and IAM roles for the custom SES resource types:CfnSESResources:Type:AWS::CloudFormation::StackProperties:TemplateURL:https://s3.amazonaws.com/YOUR_BUCKET/aws-cfn-ses-domain-VERSION.cf.yamlParameters:LambdaCodeS3Bucket:YOUR_BUCKETLambdaCodeS3Key:aws-cfn-ses-domain-VERSION.lambda.zipTheCustom::SES_DomainandCustom::SES_EmailIdentityresource types are now
available to use like this (seeUsagebelow for the full list of properties
and return values):MySESDomain:Type:Custom::SES_DomainProperties:ServiceToken:!GetAttCfnSESResources.Outputs.CustomDomainIdentityArnDomain:"example.com"# ...MySESEmailIdentity:Type:Custom::SES_EmailIdentityProperties:ServiceToken:!GetAttCfnSESResources.Outputs.CustomEmailIdentityArnEmailAddress:"sender@example.com"# ...If you'd prefer to build and upload the custom resource code from source,
see theDevelopmentsection.UsageCustom::SES_DomainTo work with aCustom::SES_Domainresource in your CloudFormation template, you'll
typically:Define the AWS Lambda Function that implements theCustom::SES_DomainCloudFormation
custom resource type, as shown inInstallationabove.Declare aCustom::SES_Domainresource for your Amazon SES domain, specifying
whatever SES options you need.Declare anAWS::Route53::RecordSetGroupresource to manage SES's
required DNS entries, passing it theRoute53RecordSetsattribute of yourCustom::SES_Domain. (Or if you're not using Route 53, use the
otherCustom::SES_Domainreturn valuesto create the appropriate
records with your DNS provider.)Here's how that looks in a cloudformation.yaml template…Resources:# 1. Define the Custom::SES_Domain's Lambda Function via a nested stack.CfnSESResources:Type:AWS::CloudFormation::StackProperties:TemplateURL:https://s3.amazonaws.com/YOUR_BUCKET/aws-cfn-ses-domain-VERSION.cf.yamlParameters:LambdaCodeS3Bucket:YOUR_BUCKETLambdaCodeS3Key:aws-cfn-ses-domain-VERSION.lambda.zip# 2. Declare a Custom::SES_Domain resource for your SES domain.MySESDomain:Type:Custom::SES_DomainProperties:# ServiceToken is the Arn of a Lambda Function from the nested stack:ServiceToken:!GetAttCfnSESResources.Outputs.CustomDomainIdentityArn# Remaining Properties are options for provisioning for your SES domain identity:# (Domain is required; all others are optional and shown with their defaults)Domain:"example.com"EnableSend:trueEnableReceive:falseMailFromSubdomain:"mail"TTL:"1800"CustomDMARC:'"v=DMARC1;p=none;pct=100;sp=none;aspf=r;"'Region:!Ref"AWS::Region"# 3. Declare a Route 53 RecordSetGroup to manage SES's required DNS entries.#    (This assumes you already have a Route 53 hosted zone for your domain;#    if not, you'll also want an AWS::Route53::HostedZone resource for it.#    Or if you don't use Route 53, see "Return Values" for other DNS options.)MyRoute53RecordsForSES:Type:AWS::Route53::RecordSetGroupProperties:HostedZoneName:"example.com."# The Route53RecordSets attribute specifies all DNS records needed:RecordSets:!GetAttMySESDomain.Route53RecordSetsPropertiesACustom::SES_Domainresource supports the following Properties:ServiceTokenThe ARN of the Lambda Function that implements theCustom::SES_Domaintype.
(This is a standard property of all CloudFormationAWS::CloudFormation::CustomResourcetypes.)If you are using a nested stack as recommended inInstallationabove,
theServiceTokenshould be set to the nested stack'sOutputs.CustomDomainIdentityArn.Note thatCustom::SES_DomainandCustom::SES_EmailIdentityusedifferentServiceToken ARNs, even though both are provided from the same nested stack.Required:YesType:StringUpdate requires:Updates are not supportedDomainThe domain name you want to provision for sending and/or receiving email via Amazon SES,
such asexample.com. (A trailing period is not required, but is allowed to simplify
working with Route 53 HostedZone names that do require it.)For more information, seeVerifying Domains in Amazon SESin theAmazon SES Developer Guide.Required:YesType:StringUpdate requires:ReplacementEnableSendWhether to enable outbound Amazon SES email from the domain. Iftrue(the default),
the resulting DNS records will include SES's required verification token and DKIM
entries.Required:NoType:BooleanDefault:trueUpdate requires:No interruptionEnableReceiveWhether to enable inbound Amazon SES email to the domain. Iftrue, the resulting
DNS records will include SES's required verification token and inbound MX entry.Required:NoType:BooleanDefault:falseUpdate requires:No interruptionMailFromSubdomainThesubdomainofDomainto use as a custom MAIL FROM domain when sending
through Amazon SES. The default ismail(so if yourCustom::SES_Domainresource hasDomain: example.com, it will by default be provisioned in SES with the custom
MAIL FROM domainmail.example.com). The resulting DNS records will include SES's
required SPF and feedback MX entries for the MAIL FROM domain.Todisableusing a custom MAIL FROM domain (and instead use SES's defaultamazonses.com), set to an empty string:MailFromSubdomain: ''.This property is only meaningful whenEnableSendistrue.For more information, seeUsing a Custom MAIL FROM Domainin theAmazon SES Developer Guide.Required:NoType:StringDefault:'mail'Update requires:No interruptionCustomDMARCA custom DMARC value to include in the resulting DNS entries. The default will enable
DMARC for your outbound email in "report only" mode. Note that youmustinclude the
double quotes around the entire string (which requires escaping in JSON or wrapping
in single quotes in YAML). Example:CustomDMARC:'"v=DMARC1;p=reject;pct=100;rua=mailto:admin@example.com"'Todisablegenerating a DMARC record, set to an empty string:CustomDMARC: ''.This property is only meaningful whenEnableSendistrue.For more information, see theDMARC.org Overview.Required:NoType:StringDefault:'"v=DMARC1; p=none; pct=100; sp=none; aspf=r;"'Update requires:No interruptionTTLThe time-to-live value to include in resulting DNS records (in seconds).Required:NoType:StringDefault:'1800'Update requires:No interruptionRegionThe AWS Region where your Amazon SES domain will be provisioned, e.g.,"us-east-1".
This must be a region where Amazon SES is supported. The default is the region where
your CloudFormation stack is running (or technically, where theCustom::SES_Domainlambda function is running.)Required:NoType:StringDefault:${AWS::Region}Update requires:ReplacementReturn ValuesRefWhen aCustom::SES_Domainresource is provided to theRefintrinsic function,Refreturns the Amazon Resource Name (ARN) of the Amazon SES domain identity
(e.g.,arn:aws:ses:us-east-1:111111111111:identity/example.com).Changed in v0.3:Earlier versions returned the domain (which is still available
as!GetAtt MySESDomain.Domain).Fn::GetAttACustom::SES_Domainresource returns severalFn::GetAttattributes that
can be used with other CloudFormation resources to maintain the required DNS records
for your Amazon SES domain.Route53RecordSetsA List ofAWS::Route53::RecordSetobjects specifying the DNS records
required for theCustom::SES_Domainidentity.This is suitable for use as theRecordSetsproperty of anAWS::Route53::RecordSetGroupresource. E.g.:MyRoute53RecordsForSES:Type:AWS::Route53::RecordSetGroupProperties:# HostedZone or HostedZoneName: ...RecordSets:!GetAttMySESDomain.Route53RecordSetsIf you update aCustom::SES_Domainresource, theRoute53RecordSetsattribute
will change accordingly, and CloudFormation will figure out the precise updates
needed to the Route 53 records.ZoneFileEntriesA List of String lines that can be used in a standardZone Fileto specify
the DNS records required for theCustom::SES_Domainidentity. Thenamefield in each
entry is a fully qualified hostname (ends in a period).Example:['_amazonses.example.com.1800INTXT"abcde12345"','abcde1._domainkey.example.com.1800INCNAMEabcde1.dkim.amazonses.com.','fghij2._domainkey.example.com.1800INCNAMEfghij2.dkim.amazonses.com.','klmno3._domainkey.example.com.1800INCNAMEklmno3.dkim.amazonses.com.','mail.example.com.1800INMX10feedback-smtp.us-west-1.amazonses.com.','mail.example.com.1800INTXT"v=spf1include:amazonses.com-all"','_dmarc.example.com.1800INTXT"v=DMARC1;p=none;pct=100;sp=none;aspf=r;"','example.com.1800INMX10inbound-smtp.us-west-1.amazonaws.com.']This can be useful if you are working with a DNS provider other than Route 53. For
example, to include the zone file entries in your stack's output, use something like:Outputs:ZoneFileEntries:Description:Add these lines to the zone file at your DNS provider.Value:!Join["\n",!GetAttMySESDomain.ZoneFileEntries]Other attributesACustom::SES_Domainresource provides several other SES-related attributes which
may be helpful for generating custom DNS records or other purposes:Domain(String): TheDomain, without any trailing period.VerificationToken(String): The VerificationToken returned fromSES:VerifyDomainIdentityDkimTokens(List of String): The list of DkimTokens returned fromSES:VerifyDomainDkim(not available ifEnableSendis false)MailFromDomain(String): the custom MAIL FROM domain, e.g.,mail.example.com(not available ifEnableSendis false
or ifMailFromSubdomainis empty)MailFromMX(String): the feedback MX host to use with a custom MAIL FROM domain,
e.g.,feedback-smtp.us-east-1.amazonses.com(not available ifEnableSendis false
or ifMailFromSubdomainis empty)MailFromSPF(String): the SPF record value to use with a custom MAIL FROM domain
(including the double quotes), e.g.,"v=spf1 include:amazonses.com -all"(not available ifEnableSendis false
or ifMailFromSubdomainis empty)DMARC(String): the DMARC record value to use for outbound mail (including the
double quotes), e.g.,"v=DMARC1; p=none; pct=100; sp=none; aspf=r;"(not available ifEnableSendis false
or ifCustomDMARCis empty)ReceiveMX(String): the inbound MX host to use for receiving email, e.g.,inbound-smtp.us-east-1.amazonaws.com(not available ifEnableReceiveis false)Arn(String): the Amazon Resource Name (ARN) of the provisioned Amazon SES
domain identity (useful for delegating sending authorization; this is the same
value returned by!Ref MySESDomain)Region(String): the resolvedRegionwhere the Amazon SES domain
was provisionedCustom::SES_EmailIdentityTo work with aCustom::SES_EmailIdentityresource in your CloudFormation template,
you'll typically:Define the AWS Lambda Function that implements theCustom::SES_EmailIdentityCloudFormation custom resource type, as shown inInstallationabove.Declare one or moreCustom::SES_EmailIdentityresource for your the email addresses
you need to verify.Here's how that looks in a cloudformation.yaml template…Resources:# 1. Define the Custom::SES_EmailIdentity's Lambda Function via a nested stack.CfnSESResources:Type:AWS::CloudFormation::StackProperties:TemplateURL:https://s3.amazonaws.com/YOUR_BUCKET/aws-cfn-ses-domain-VERSION.cf.yamlParameters:LambdaCodeS3Bucket:YOUR_BUCKETLambdaCodeS3Key:aws-cfn-ses-domain-VERSION.lambda.zip# 2. Declare Custom::SES_EmailIdentity resources for each address to verify.MySESSender:Type:Custom::SES_EmailIdentityProperties:# ServiceToken is the Arn of a Lambda Function from the nested stack:ServiceToken:!GetAttCfnSESResources.Outputs.CustomEmailIdentityArn# Remaining Properties are options for verifying the email address:# (EmailAddress is required; all others are optional and shown with their defaults)EmailAddress:"sender@example.com"Region:!Ref"AWS::Region"MySESReplyTo:Type:Custom::SES_EmailIdentityProperties:ServiceToken:!GetAttCfnSESResources.Outputs.CustomEmailIdentityArnEmailAddress:"noreply@example.com"PropertiesServiceTokenThe ARN of the Lambda Function that implements theCustom::SES_EmailIdentitytype.
(This is a standard property of all CloudFormationAWS::CloudFormation::CustomResourcetypes.)If you are using a nested stack as recommended inInstallationabove,
theServiceTokenshould be set to the nested stack'sOutputs.CustomEmailIdentityArn.Note thatCustom::SES_EmailIdentityandCustom::SES_DomainusedifferentServiceToken ARNs, even though both are provided from the same nested stack.Required:YesType:StringUpdate requires:Updates are not supportedEmailAddressThe email address you want to verify for sending through Amazon SES, such assender@example.com. This cannot include any "display name", and must be a single
email address. (If you need to verify multiple addresses, simply create as manyCustom::SES_EmailAddressresources as needed.)AWS will send a verification email to this address the first time the stack containing
theCustom::SES_EmailAddressresource is deployed, and again on any stack updates
that alter the resource (such as changing itsRegion).For more information, seeVerifying Email Addresses in Amazon SESin theAmazon SES Developer Guide.Required:YesType:StringUpdate requires:ReplacementRegionThe AWS Region where the email address will be verified, e.g.,"us-east-1".
This must be a region where Amazon SES is supported. The default is the region where
your CloudFormation stack is running (or technically, where theCustom::SES_EmailIdentitylambda function is running.)Required:NoType:StringDefault:${AWS::Region}Update requires:ReplacementReturn ValuesRefWhen aCustom::SES_EmailIdentityresource is provided to theRefintrinsic function,Refreturns the Amazon Resource Name (ARN) of the Amazon SES email identity
(e.g.,arn:aws:ses:us-east-1:111111111111:identity/sender@example.com).Fn::GetAttACustom::SES_EmailIdentityresource returns a fewFn::GetAttattributes
that may be helpful for working with it elsewhere in your template.Arn(String): the Amazon Resource Name (ARN) of the Amazon SES email identity
(this is the same value returned by!Ref MySESEmailAddress)EmailAddress(String): theEmailAddressthat was verifiedRegion(String): the resolvedRegionwhere the email identity
was verifiedValidating Your TemplatesIf you usecfn-lint(recommended!) to check your CloudFormation templates,
you can include an "override spec" so yourCustom::SES_DomainandCustom::SES_EmailIdentityproperties and attributes will be validated. Download a
copy ofCustomSESDomainSpecification.jsonand then:cfn-lint--override-specCustomSESDomainSpecification.jsonYOUR-TEMPLATE.cf.yaml(Without the override-spec, cfn-lint will allowanyproperties and values forCustom::SES_DomainandCustom::SES_EmailIdentityresources.)DevelopmentDevelopment requires GNU Make (standard on most Linux-like systems) and Python 3.
Ideally—particularly if you are changing code and running tests—you'll use the same
version of Python that the Lambda Functions run (check theRuntimedirectives in
[aws-cfn-ses-domain.cf.yaml][./aws-cfn-ses-domain.cf.yaml]), but any Python 3 should
be fine.To set up your development environment:Clone the repository.(Recommended) create and activate a Python venv for development.
(pyenvandpyenv-virtualenvare very helpful for this.)Runmake init. (This just runspip3 install -r requirements-dev.txt.)To see a list of available make targets, runmake help.To package and upload your own version of the Lambda zip package and the CloudFormation
templates, runmake S3_BUCKET=your_bucket_name upload. If you just want to build
locally without uploading to S3, runmake S3_BUCKET=your_bucket_name all. You can also
includeS3_PREFIX=your/s3/prefixorS3_PREFIX=in either of these commands,
if desired.If you are changing code, you will want to run tests (make test) and static code
checks (make check) before uploading.Additional development customization variables are documented near the top
of the Makefile.AlternativesThese packages offer similar functionality (and provided some helpful background
forCustom::SES_Domain):https://medium.com/poka-techblog/verify-domains-for-ses-using-cloudformation-8dd185c9b05chttps://github.com/binxio/cfn-ses-providerBoth of these manage your Route 53 records directly from the Lambda Function
(rather than leaving that to other CloudFormation resources), and they may support
fewer Amazon SES domain identity options than this package.FutureTheCustom::SES_Domainimplementation is currently missing these Amazon SES
domain identity features:Control over SNS feedback notifications and forwarding options
(SES:SetIdentityNotificationTopic and SES:SetIdentityFeedbackForwardingEnabled)Control over Easy DKIM enabling (SES:SetIdentityDkimEnabled—currently,Custom::SES_Domainassumes if you are enabling sending, you also want Easy DKIM)And theCustom::SES_EmailIdentityimplementation is currently missing this Amazon SES
email identity feature:Ability to use a custom verification email template (SES:SendCustomVerificationEmail)
and configuration set when verifying an email address.Adding them is likely straightforward; contributions are welcome.Are you from Amazon? It'd be great to have the SES domain and email identity resources
standard in CloudFormation. Please consider adopting or obsoleting this package.
(Just reach out if you'd like me to assign or transfer it.)