简单的记录一下最近完成的一个任务。
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个晚上才恢复。
为了避免这种事情发生,我们需要定期的监控,如果发现指标接近临界值,就立刻给管理员发送警告。管理员收到警告之后,需要能够查看日志记录,定位出问题的邮件。在极端情况下,如果账号泄露导致发送大量异常垃圾邮件,而AWS又真的停止了这个服务,需要和客服交流,更换所有的IAM role,限制IP等操作,再要求AWS放行。 监控Metric的警报在cloudwatch里面就可以很容易的实现。当警报触发时候,发送给对应的邮件通知SNS Topic就可以了。
当然,我们也可以用第三方的软件,比如Nagios,他本身有很多针对AWS的插件,也可以用来监控。Nagios的好处是他的告警系统很强,可以给值班的人发送短信通知,而且价格比aws的便宜。
记录SES的邮件日志
这个是重难点。SES默认不会给你长期保存记录单的,因此需要自己处理一下。目前豆子公司对于发送出去的SES邮件,每次都会触发 SNS的 一个Topic,这个Topic 又绑定了2个不同的lambda 函数,分别把这个邮件写入 Dynamodb 和 S3 中保存
SES里面的notification 发送对应的事件给SNS topic
SNS里面的topic,一个是发邮件给teams channel,一个是触发Lambda
Dynamodb
SES记录到Dynamodb的示意图
首先创建一个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,)
}
});
结果如下所示
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的效果图
汇总的报告
有的时候,需要看看最近一段时间的SES的Metric状态,可以通过AWS CLI来获取。下面的命令可以输出一个状态报告,因为他的结果本身是一个JSON格式,我们可以直接在Excel里面转换打开,一目了然。
aws ses get-send-statistics --region us-west-2