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

Ele*_*Ele 4 amazon-web-services node.js amazon-sns aws-lambda aws-api-gateway

我们在 Backend 类中有一个服务,该服务如下所示:

// 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'
    }
};
Run Code Online (Sandbox Code Playgroud)

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

以下是不好的地方,我们知道:

  • 我们在 EC2 中使用密钥。但是,我们正在努力为实例设置具有特定权限的角色。

  • 想象一下,我们需要修改发送 SMS 的方式,我们必须为应用程序的那一小部分重新部署整个应用程序。

  • 最糟糕的是,假设我们在 AutoScaling 上有我们的应用程序。我们必须删除所有实例才能更新我们应用程序的那一小部分。

  • 另一个问题是,如果我们必须在其他应用程序中使用该服务怎么办?当前的方法导致在应用程序之间复制服务。

  • 最后,如何进行日志记录、监控等。

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

Ele*_*Ele 7

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

这种架构允许您提供一个 Restful Endpoint,它将消息传递给特定的接收者。此微服务可以从您的应用程序、设备应用程序等的不同部分执行,因此不仅限于一个后端用途。

##架构如下###详细视图 在此处输入图片说明


###简单视图

在此处输入图片说明


#解释

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

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

{
   "target": "554542121245",
   "type": "sms",
   "message": "Hello World!",
   "region": "us-east-1"
}
Run Code Online (Sandbox Code Playgroud)
  1. API 网关验证 API 以授予访问权限并将接收到的负载发送到 Lambda 函数。

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

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

{
    "status": 200,
    "message": "The message has been sent!"
}
           
Run Code Online (Sandbox Code Playgroud)
  1. API 网关评估响应并将响应发送回调用者。
    • API 网关具有检查从 Lambda 函数发送的响应类型的智能。
    • 对于以 412 开头的响应意味着Precondition Failed
    • 对于以 500 开头的响应意味着Internal server error

Lambda 代码(NodeJ)

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}`;
   }
};
Run Code Online (Sandbox Code Playgroud)

Cloudformation 模板

此 cloudformation 模板描述了整套服务、API 网关、Lambda 函数、角色、权限、API 的使用计划、API 密钥等。

下载请点击 这里

{
    "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"