简单的记录一下最近完成的一个任务。

AWS SES服务可以允许开发人员在软件中发送批量的邮件通知。具体怎么配置的,这里就简单概述一下,主要是记录一下他的监控和日志的配置和实现。

配置

AWS 控制台 -> Services - > SES 创建域名,确认DKIM,更新DNS记录 配置SMTP 联系AWS客服移除SES邮件限制

监控和告警

下面看看重点。SES批量发送邮件很容易被对方的邮件判断成垃圾邮件,因此他有Metrics里面两个指标,一个是bounce rate,一个是 complaint rate。前者是接收方的邮箱不存在或者无法发送导致反弹回来,后者是对方认为SES是垃圾邮件,回复一个抗议的信息。当bounce rate和 complaint rate超过一定的比例之后,AWS会发送通知,如果置之不理,那么再达到临界值,这个服务就会被AWS禁止掉。豆子公司就曾经发生过这种意外,导致服务被AWS强制停止,折腾了1个晚上才恢复。

image.png

为了避免这种事情发生,我们需要定期的监控,如果发现指标接近临界值,就立刻给管理员发送警告。管理员收到警告之后,需要能够查看日志记录,定位出问题的邮件。在极端情况下,如果账号泄露导致发送大量异常垃圾邮件,而AWS又真的停止了这个服务,需要和客服交流,更换所有的IAM role,限制IP等操作,再要求AWS放行。 监控Metric的警报在cloudwatch里面就可以很容易的实现。当警报触发时候,发送给对应的邮件通知SNS Topic就可以了。

image.png

当然,我们也可以用第三方的软件,比如Nagios,他本身有很多针对AWS的插件,也可以用来监控。Nagios的好处是他的告警系统很强,可以给值班的人发送短信通知,而且价格比aws的便宜。

image.png

记录SES的邮件日志

这个是重难点。SES默认不会给你长期保存记录单的,因此需要自己处理一下。目前豆子公司对于发送出去的SES邮件,每次都会触发 SNS的 一个Topic,这个Topic 又绑定了2个不同的lambda 函数,分别把这个邮件写入 Dynamodb 和 S3 中保存

image.png SES里面的notification 发送对应的事件给SNS topic

image.png

SNS里面的topic,一个是发邮件给teams channel,一个是触发Lambda

Dynamodb

SES记录到Dynamodb的示意图

image.png

首先创建一个dynamoDB的表,包括下面的属性

Table-name: SESNotifications. primary Partition key: SESMessageId. primary Sort key: SnsPublishTime.

然后,为了快速查询,我们需要添加secondary index。毕竟dynamodb的卖点就在于快速查询,如果只有SESMessageID来扫描的话就太慢了

Index name Partition key Sort key SESMessageType-Index SESMessageType (String) SnsPublishTime (String) SESMessageComplaintType-Index SESComplaintFeedbackType (String) SnsPublishTime (String)

第三步,配置IAM Role

IAM Role

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:us-west-2:065816533466:table/SESNotifications"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

最后是 Lambda 脚本

console.log("Loading event");

var aws = require("aws-sdk"); var ddb = new aws.DynamoDB({ params: { TableName: "SESNotifications" } });

exports.handler = function (event, context, callback) {

 console.log("Received event:", JSON.stringify(event, null, 2));
 var SnsPublishTime = event.Records[0].Sns.Timestamp;
 var SnsTopicArn = event.Records[0].Sns.TopicArn;
 var SESMessage = event.Records[0].Sns.Message;
 SESMessage = JSON.parse(SESMessage);
 var SESMessageType = SESMessage.notificationType;
 var SESMessageId = SESMessage.mail.messageId;
 var SESDestinationAddress = SESMessage.mail.destination.toString();
 var LambdaReceiveTime = new Date().toString();
 var SESSubject = SESMessage.mail.commonHeaders.subject;
 console.log("Type is ", SESMessageType)
 console.log("Subject is ", SESSubject)
 if (SESMessageType == "Bounce") {
   var SESreportingMTA = SESMessage.bounce.reportingMTA;
   var SESbounceSummary = JSON.stringify(SESMessage.bounce.bouncedRecipients);
   var itemParams = {
     Item: {
       SESMessageId: { S: SESMessageId },
       SnsPublishTime: { S: SnsPublishTime },
       SESreportingMTA: { S: SESreportingMTA },
       SESDestinationAddress: { S: SESDestinationAddress },
       SESbounceSummary: { S: SESbounceSummary },
       SESMessageType: { S: SESMessageType },
       SESSubject: { S: SESSubject },
     },
   };
   ddb.putItem(itemParams, function (err, data) {
     if (err) {
       callback(err)
     } else {
       console.log(data);
       callback(null,)
     }
   });
 } else if (SESMessageType == "Delivery") {
   console.log('Create Delivery info')
   // var SESsmtpResponse1 = SESMessage.delivery.smtpResponse;
   // var SESreportingMTA1 = SESMessage.delivery.reportingMTA;
   // var itemParamsdel = {
   //   Item: {
   //     SESMessageId: { S: SESMessageId },
   //     SnsPublishTime: { S: SnsPublishTime },
   //     SESsmtpResponse: { S: SESsmtpResponse1 },
   //     SESreportingMTA: { S: SESreportingMTA1 },
   //     SESDestinationAddress: { S: SESDestinationAddress },
   //     SESMessageType: { S: SESMessageType },
   //     SESSubject: { S: SESSubject },
  
   //   },
   // };
   // console.log(itemParamsdel)
   // ddb.putItem(itemParamsdel, function (err, data) {
   //   if (err) {
   //     console.log(err)
   //     callback(err)
   //   } else {
   //     console.log(data);
   //     callback(null,)
   //   }
   // });
 } else if (SESMessageType == "Complaint") {
   var SESComplaintFeedbackType = SESMessage.complaint.complaintFeedbackType;
   var SESFeedbackId = SESMessage.complaint.feedbackId;
   var itemParamscomp = {
     Item: {
       SESMessageId: { S: SESMessageId },
       SnsPublishTime: { S: SnsPublishTime },
       SESComplaintFeedbackType: { S: SESComplaintFeedbackType },
       SESFeedbackId: { S: SESFeedbackId },
       SESDestinationAddress: { S: SESDestinationAddress },
       SESMessageType: { S: SESMessageType },
       SESSubject: { S: SESSubject },
     },
   };
   ddb.putItem(itemParamscomp, function (err, data) {
     if (err) {
       callback(err)
     } else {
       console.log(data);
       callback(null,)
     }
   });

结果如下所示

image.png

S3

类似的,我可以配置把当天的邮件汇总写入S3的一个key中

S3 bucket access policy

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Effect": "Allow",
           "Action": [
               "s3:*"
           ],
           "Resource": [
               "arn:aws:s3:::ses-outgoing-emails",
               "arn:aws:s3:::ses-outgoing-emails/*"
           ]
       }
   ]
}

Lambda 权限

创建一个role 绑定默认 AWSLambdaBasicExecutionRole的 Policy即可

#### Lambda 代码

console.log('Loading function');

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var AWS = require('aws-sdk');
    AWS.config.update({region:'us-west-2'});

// get curret AEDT time
    process.env.TZ = 'Australia/Sydney';
    var mytimestamp = new Date();
    var year = mytimestamp.getFullYear();
    var month = mytimestamp.getMonth();
    month += 1;
    var day = mytimestamp.getDate();
    var hours = mytimestamp.getHours();
    var minutes = mytimestamp.getMinutes();
    var seconds = mytimestamp.getSeconds();

// set bucket=ses-outgoing-emails/yyyy-mm-dd, filename=yyyy-mm-dd-hour-minute-second
    var bucketname = 'ses-outgoing-emails/'+ year + '-' + month + '-' + day;
    var myfilename = 'Outgoing-' + year + '-' + month + '-' + day + '-' + hours + minutes + seconds;

// parse SNS message    
    var alarm_msg = event.Records[0].Sns.Message;
    var params = {Key: myfilename, Body: 'Message Part 1. Here is the message payload'+alarm_msg+'MORETHINGS'};

// init s3 connection
    var s3bucket = new AWS.S3({params: {Bucket: bucketname}});

// start uploading
    s3bucket.putObject(params, function (error, data) {

    if (error) {
        console.log("Error in posting Content [" + error + "]");
        return false;
    } /* end if error */
    else {
        console.log("Successfully posted Content");
    } /* end else error */
})
};

S3的效果图 image.png

汇总的报告

有的时候,需要看看最近一段时间的SES的Metric状态,可以通过AWS CLI来获取。下面的命令可以输出一个状态报告,因为他的结果本身是一个JSON格式,我们可以直接在Excel里面转换打开,一目了然。

aws ses get-send-statistics --region us-west-2

image.png