是否有部署架构以使用微服务模型发送 SMS 的最佳方法?

Is there a best approach to deploy an architecture to send SMS using a Microservice model?

我们在后端 class 中有一个服务,该服务看起来像:

// Setup AWS SNS
AWS.config.update({
    region: 'eu-west-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
});
var sns = new AWS.SNS();

var params = {
    Message: "SMS message test",
    MessageStructure: 'string',
    PhoneNumber: '0045xxxxxxxx',
    Subject: 'Alarm',
    MessageAttributes :{
        'AWS.SNS.SMS.SenderID': {
            'DataType': 'String',
            'StringValue': 'MySender'
        },
        'AWS.SNS.SMS.SMSType': 'Transactional'
    }
};

如果我们需要发送短信,只需调用此服务即可。

这里不好的是下面的,我们知道的:

我们认为有更好的方法可以避免此类问题,所以您可以查看我们避免上述问题的方法。

经过几个小时的头脑风暴,我们决定使用 AWS 的四项基本服务

This architecture allows you to provide a Restful Endpoint which delivers a message to a specific receiver. This microservice could be executed from different parts of your application, device apps, Etc., so isn't tied to only one Backend purpose.

##架构如下 ###详细视图


###简单视图


#解释

我们将逐步描述发送 SMS 的流程。

  1. 源需要向特定的电话phone号码发送消息,因此调用方执行POST请求(/delivermessage),并向API网关发送以下有效负载终点

{
   "target": "554542121245",
   "type": "sms",
   "message": "Hello World!",
   "region": "us-east-1"
}

  1. API 网关验证 API 以授予访问权限并将接收到的负载发送到 Lambda 函数。

  2. Lambda 函数验证接收到的负载并执行以下操作:

    • 创建 SNS 主题。
    • 使用收到的电话号码创建订阅phone。
    • 订阅主题。
    • 通过该订阅发布消息。
    • 删除订阅。
    • 删除主题。
    • Returns 向调用方返回成功响应:

{
    "status": 200,
    "message": "The message has been sent!"
}
           

  1. API 网关评估响应并将响应发送回调用方。
    • API 网关具有智能功能,可以检查从 Lambda 函数发送的响应类型。
    • 对于以 412 开头的响应意味着 前提条件失败
    • 对于以 500 开头的响应意味着 内部服务器错误

Lambda 代码(NodeJs)

var AWS = require('aws-sdk');

/**
 * Entry function for this
 * Lambda.
 * 
 * This function delivers a message 
 * to a specific number.
 * 
 * First approach will only handle 
 * delivery type sms.
 */
exports.handler = (event, context, callback) => {
    console.log(JSON.stringify(event));

    if (event.type === undefined || event.type === null || event.type === '' || event.type.trim() === '') {
        callback(get_response_message('Type of delivery is required.'), 412);
        return;
    }
   
    if (event.type.trim() !== 'sms') {
        callback(get_response_message('The available delivery type is \'sms\'.', 412));
        return;
    }

    if (event.type.trim() === 'sms' && (event.target === '' || isNaN(event.target))) {
        callback(get_response_message('The target must be a number.', 412));
        return;
    }

    deliver(event.target, event.message, event.region, callback);
};

/**
 * This function delivers a
 * message to a specific number.
 * 
 * The function will create a topic
 * from scratch to avoid any
 * clash among subscriptions.
 * 
 * @param number in context.
 * @param message that will be sent.
 * @param region in context.
 * @param cb a callback function to 
 *           return a response to the 
 *           caller of this service.
 */
var deliver = (number, message, region, cb) => {
   var sns = new AWS.SNS({region: region});
   console.log(`${number} - ${region} - ${Date.now()}`);
   var params = { Name: `${number}_${region}_${Date.now()}` };

   sns.createTopic(params, function(err, tdata) {
     if (err) {
         console.log(err, err.stack);
         cb(get_response_message(err, 500));
     } else {
         console.log(tdata.TopicArn);
         sns.subscribe({
           Protocol: 'sms',
           TopicArn: tdata.TopicArn,
           Endpoint: number
       }, function(error, data) {
            if (error) {
                //Rollback to the previous created services.
                console.log(error, error.stack);
                params = { TopicArn: tdata.TopicArn};
                sns.deleteTopic(params, function() { cb(get_response_message(error, 500)); });

                return;
            }

            console.log('subscribe data', data);
            var SubscriptionArn = data.SubscriptionArn;

            params = { TargetArn: tdata.TopicArn, Message: message, Subject: 'dummy' };
            sns.publish(params, function(err_publish, data) {
               if (err_publish) {
                    console.log(err_publish, err_publish.stack);
                    //Rollback to the previous created services.
                    params = { TopicArn: tdata.TopicArn};
                    sns.deleteTopic(params, function() {
                        params = {SubscriptionArn: SubscriptionArn};
                        sns.unsubscribe(params, function() { cb(get_response_message(err_publish, 500)); });
                    });

                    return;
               } else console.log('Sent message:', data.MessageId);

               params = { SubscriptionArn: SubscriptionArn };
               sns.unsubscribe(params, function(err, data) {
                  if (err) console.log('err when unsubscribe', err);

                  params = { TopicArn: tdata.TopicArn };
                  sns.deleteTopic(params, function(rterr, rtdata) {
                     if (rterr) {
                        console.log(rterr, rterr.stack);
                        cb(get_response_message(rterr, 500));
                     } else {
                        console.log(rtdata);
                        cb(null, get_response_message('Message has been sent!', 200));
                     }
                  });
               });
           });
         });
      }
   });
};

/**
 * This function returns the response
 * message that will be sent to the 
 * caller of this service.
 */
var get_response_message = (msg, status) => {
   if (status == 200) {
      return `{'status': ${status}, 'message': ${msg}}`;
   } else {
      return `${status} - ${msg}`;
   }
};

Cloudformation 模板

This cloudformation template describes the whole set of services, API Gateway, Lambda function, Roles, Permissions, Usage plans for the API, API Key, Etc.

下载点击 here

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "This template deploys the necessary resources for sending MSG through a API-Gateway endpoint, Lambda function and SNS service.",
    "Metadata": {
        "License": {
            "Description": "MIT license - Copyright (c) 2017"
        }
    },
    "Resources": {
        "LambdaRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Policies": [
                    {
                        "PolicyName": "LambdaSnsNotification",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Sid": "AllowSnsActions",
                                    "Effect": "Allow",
                                    "Action": [
                                        "sns:Publish",
                                        "sns:Subscribe",
                                        "sns:Unsubscribe",
                                        "sns:DeleteTopic",
                                        "sns:CreateTopic"
                                    ],
                                    "Resource": "*"
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "LambdaFunctionMessageSNSTopic": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Description": "Send message to a specific topic that will deliver MSG to a receiver.",
                "Handler": "index.handler",
                "MemorySize": 128,
                "Role": {
                    "Fn::GetAtt": [
                        "LambdaRole",
                        "Arn"
                    ]
                },
                "Runtime": "nodejs6.10",
                "Timeout": 60,
                "Environment": {
                    "Variables": {
                        "sns_topic_arn": ""
                    }
                },
                "Code": {
                    "ZipFile": {
                        "Fn::Join": [
                            "\n",
                            [
                                "var AWS = require('aws-sdk');",
                                "",
                                "/**",
                                " * Entry function for this",
                                " * Lambda.",
                                " * ",
                                " * This function delivers a message ",
                                " * to a specific number.",
                                " * ",
                                " * First approach will only handle ",
                                " * delivery type sms.",
                                " */",
                                "exports.handler = (event, context, callback) => {",
                                "    console.log(JSON.stringify(event));",
                                "",
                                "    if (event.type === undefined || event.type === null || event.type === '' || event.type.trim() === '') {",
                                "        callback(get_response_message('Type of delivery is required.'), 412);",
                                "        return;",
                                "    }",
                                "   ",
                                "    if (event.type.trim() !== 'sms') {",
                                "        callback(get_response_message('The available delivery type is \'sms\'.', 412));",
                                "        return;",
                                "    }",
                                "",
                                "    if (event.type.trim() === 'sms' && (event.target === '' || isNaN(event.target))) {",
                                "        callback(get_response_message('The target must be a number.', 412));",
                                "        return;",
                                "    }",
                                "",
                                "    deliver(event.target, event.message, event.region, callback);",
                                "};",
                                "",
                                "/**",
                                " * This function delivers a",
                                " * message to a specific number.",
                                " * ",
                                " * The function will create a topic",
                                " * from scratch to avoid any",
                                " * clash among subscriptions.",
                                " * ",
                                " * @param number in context.",
                                " * @param message that will be sent.",
                                " * @param region in context.",
                                " * @param cb a callback function to ",
                                " *           return a response to the ",
                                " *           caller of this service.",
                                " */",
                                "var deliver = (number, message, region, cb) => {",
                                "   var sns = new AWS.SNS({region: region});",
                                "   console.log(`${number} - ${region} - ${Date.now()}`);",
                                "   var params = { Name: `${number}_${region}_${Date.now()}` };",
                                "",
                                "   sns.createTopic(params, function(err, tdata) {",
                                "     if (err) {",
                                "         console.log(err, err.stack);",
                                "         cb(get_response_message(err, 500));",
                                "     } else {",
                                "         console.log(tdata.TopicArn);",
                                "         sns.subscribe({",
                                "           Protocol: 'sms',",
                                "           TopicArn: tdata.TopicArn,",
                                "           Endpoint: number",
                                "       }, function(error, data) {",
                                "            if (error) {",
                                "               //Rollback to the previous created services.",
                                "                console.log(error, error.stack);",
                                "               params = { TopicArn: tdata.TopicArn};",
                                "               sns.deleteTopic(params, function() { cb(get_response_message(error, 500)); });",
                                "",
                                "               return;",
                                "            }",
                                "",
                                "            console.log('subscribe data', data);",
                                "            var SubscriptionArn = data.SubscriptionArn;",
                                "",
                                "            params = { TargetArn: tdata.TopicArn, Message: message, Subject: 'dummy' };",
                                "            sns.publish(params, function(err_publish, data) {",
                                "               if (err_publish) {",
                                "                    console.log(err_publish, err_publish.stack);",
                                "                   //Rollback to the previous created services.",
                                "                   params = { TopicArn: tdata.TopicArn};",
                                "                   sns.deleteTopic(params, function() {",
                                "                       params = {SubscriptionArn: SubscriptionArn};",
                                "                       sns.unsubscribe(params, function() { cb(get_response_message(err_publish, 500)); });",
                                "                   });",
                                "",
                                "                    return;",
                                "               } else console.log('Sent message:', data.MessageId);",
                                "",
                                "               params = { SubscriptionArn: SubscriptionArn };",
                                "               sns.unsubscribe(params, function(err, data) {",
                                "                  if (err) console.log('err when unsubscribe', err);",
                                "",
                                "                  params = { TopicArn: tdata.TopicArn };",
                                "                  sns.deleteTopic(params, function(rterr, rtdata) {",
                                "                     if (rterr) {",
                                "                        console.log(rterr, rterr.stack);",
                                "                        cb(get_response_message(rterr, 500));",
                                "                     } else {",
                                "                        console.log(rtdata);",
                                "                        cb(null, get_response_message('Message has been sent!', 200));",
                                "                     }",
                                "                  });",
                                "               });",
                                "           });",
                                "         });",
                                "      }",
                                "   });",
                                "};",
                                "",
                                "/**",
                                " * This function returns the response",
                                " * message that will be sent to the ",
                                " * caller of this service.",
                                " */",
                                "var get_response_message = (msg, status) => {",
                                "   if (status == 200) {",
                                "      return `{'status': ${status}, 'message': ${msg}}`;",
                                "   } else {",
                                "      return `${status} - ${msg}`;",
                                "   }",
                                "};"
                            ]
                        ]
                    }
                }
            }
        },
        "MSGGatewayRestApi": {
            "Type": "AWS::ApiGateway::RestApi",
            "Properties": {
                "Name": "MSG RestApi",
                "Description": "API used for sending MSG",
                "FailOnWarnings": true
            }
        },
        "MSGGatewayRestApiUsagePlan": {
            "Type": "AWS::ApiGateway::UsagePlan",
            "Properties": {
                "ApiStages": [
                    {
                        "ApiId": {
                            "Ref": "MSGGatewayRestApi"
                        },
                        "Stage": {
                            "Ref": "MSGGatewayRestApiStage"
                        }
                    }
                ],
                "Description": "Usage plan for stage v1",
                "Quota": {
                    "Limit": 5000,
                    "Period": "MONTH"
                },
                "Throttle": {
                    "BurstLimit": 200,
                    "RateLimit": 100
                },
                "UsagePlanName": "Usage_plan_for_stage_v1"
            }
        },
        "RestApiUsagePlanKey": {
            "Type": "AWS::ApiGateway::UsagePlanKey",
            "Properties": {
                "KeyId": {
                    "Ref": "MSGApiKey"
                },
                "KeyType": "API_KEY",
                "UsagePlanId": {
                    "Ref": "MSGGatewayRestApiUsagePlan"
                }
            }
        },
        "MSGApiKey": {
            "Type": "AWS::ApiGateway::ApiKey",
            "Properties": {
                "Name": "MSGApiKey",
                "Description": "CloudFormation API Key v1",
                "Enabled": "true",
                "StageKeys": [
                    {
                        "RestApiId": {
                            "Ref": "MSGGatewayRestApi"
                        },
                        "StageName": {
                            "Ref": "MSGGatewayRestApiStage"
                        }
                    }
                ]
            }
        },
        "MSGGatewayRestApiStage": {
            "DependsOn": [
                "ApiGatewayAccount"
            ],
            "Type": "AWS::ApiGateway::Stage",
            "Properties": {
                "DeploymentId": {
                    "Ref": "RestAPIDeployment"
                },
                "MethodSettings": [
                    {
                        "DataTraceEnabled": true,
                        "HttpMethod": "*",
                        "LoggingLevel": "INFO",
                        "ResourcePath": "/*"
                    }
                ],
                "RestApiId": {
                    "Ref": "MSGGatewayRestApi"
                },
                "StageName": "v1"
            }
        },
        "ApiGatewayCloudWatchLogsRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "apigateway.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Policies": [
                    {
                        "PolicyName": "ApiGatewayLogsPolicy",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:CreateLogGroup",
                                        "logs:CreateLogStream",
                                        "logs:DescribeLogGroups",
                                        "logs:DescribeLogStreams",
                                        "logs:PutLogEvents",
                                        "logs:GetLogEvents",
                                        "logs:FilterLogEvents"
                                    ],
                                    "Resource": "*"
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "ApiGatewayAccount": {
            "Type": "AWS::ApiGateway::Account",
            "Properties": {
                "CloudWatchRoleArn": {
                    "Fn::GetAtt": [
                        "ApiGatewayCloudWatchLogsRole",
                        "Arn"
                    ]
                }
            }
        },
        "RestAPIDeployment": {
            "Type": "AWS::ApiGateway::Deployment",
            "DependsOn": [
                "MSGGatewayRequest"
            ],
            "Properties": {
                "RestApiId": {
                    "Ref": "MSGGatewayRestApi"
                },
                "StageName": "DummyStage"
            }
        },
        "ApiGatewayMSGResource": {
            "Type": "AWS::ApiGateway::Resource",
            "Properties": {
                "RestApiId": {
                    "Ref": "MSGGatewayRestApi"
                },
                "ParentId": {
                    "Fn::GetAtt": [
                        "MSGGatewayRestApi",
                        "RootResourceId"
                    ]
                },
                "PathPart": "delivermessage"
            }
        },
        "MSGGatewayRequest": {
            "DependsOn": "LambdaPermission",
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                "ApiKeyRequired": true,
                "AuthorizationType": "NONE",
                "HttpMethod": "POST",
                "Integration": {
                    "Type": "AWS",
                    "IntegrationHttpMethod": "POST",
                    "Uri": {
                        "Fn::Join": [
                            "",
                            [
                                "arn:aws:apigateway:",
                                {
                                    "Ref": "AWS::Region"
                                },
                                ":lambda:path/2015-03-31/functions/",
                                {
                                    "Fn::GetAtt": [
                                        "LambdaFunctionMessageSNSTopic",
                                        "Arn"
                                    ]
                                },
                                "/invocations"
                            ]
                        ]
                    },
                    "IntegrationResponses": [
                        {
                            "StatusCode": 200
                        },
                        {
                            "SelectionPattern": "500.*",
                            "StatusCode": 500
                        },
                        {
                            "SelectionPattern": "412.*",
                            "StatusCode": 412
                        }
                    ],
                    "RequestTemplates": {
                        "application/json": ""
                    }
                },
                "RequestParameters": {
                },
                "ResourceId": {
                    "Ref": "ApiGatewayMSGResource"
                },
                "RestApiId": {
                    "Ref": "MSGGatewayRestApi"
                },
                "MethodResponses": [
                    {
                        "StatusCode": 200
                    },
                    {
                        "StatusCode": 500
                    },
                    {
                        "StatusCode": 412
                    }
                ]
            }
        },
        "LambdaPermission": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "Action": "lambda:invokeFunction",
                "FunctionName": {
                    "Fn::GetAtt": [
                        "LambdaFunctionMessageSNSTopic",
                        "Arn"
                    ]
                },
                "Principal": "apigateway.amazonaws.com",
                "SourceArn": {
                    "Fn::Join": [
                        "",
                        [
                            "arn:aws:execute-api:",
                            {
                                "Ref": "AWS::Region"
                            },
                            ":",
                            {
                                "Ref": "AWS::AccountId"
                            },
                            ":",
                            {
                                "Ref": "MSGGatewayRestApi"
                            },
                            "/*"
                        ]
                    ]
                }
            }
        }
    }
}

#Received SMS in my phone executing a request to the API Gateway endpoint

更新 - 2021

SNS js sdk 提供了一种无需创建主题即可直接发送短信的方式。

如果您的用例是向个人发送单个短信,则您无需创建主题并在之后将其删除。可以使用以下代码简单地发送一条短信。

let AWS = require('aws-sdk');
const sns = new AWS.SNS();
exports.handler = function (event, context, callback) {
    var params = {
  Message: event.message, //  your message you would like to send
        MessageAttributes: {
            'AWS.SNS.SMS.SMSType': {
                DataType: 'String',
                StringValue: event.messageType // the smsType "Transactional" or "Promotional"
            },
            'AWS.SNS.SMS.SenderID': {
                DataType: 'String',
                StringValue: event.messageSender // your senderId - the message that will show up as the sender on the receiving phone
            },
        },
  PhoneNumber: event.phone // the phone number of the receiver 
};

sns.publish(params, function (err, data) {
        callback(null, {err: err, data: data});
        if (err) {
            console.log(err);
            context.fail(err);
        } else {
            console.log("Send sms successful to user:", event.phone);
            context.succeed(event);
            return;
        }
    });
};

api endpoint/lambda 收到以下正文

{
"message": "hey ho I am the sms message.",
"messageType": "Transactional", //or "Promotional"
"messageSender": "Your Brand",
"phone":"+436640339333"
}