问题
We have a service within a Backend class, the service looks like:
// 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'
}
};
If we need to send an SMS, we simply call this service.
What is bad here is the following, and we know it:
We're using the Secret keys within the EC2. However, we're working on that for setting a role with the specific permissions to the instances.
Imagine we need to modify the way we are sending SMS, we would have to re-deploy the entire app just for that tiny part of our application.
Worst, imagine we have our application on AutoScaling. We would have to drop off all the instances just to update that tiny part of our application.
Another problem is, what if we have to use that service in other applications? The current approach leads to duplicate a service among applications.
Last, how to logging, monitoring, Etc.
We think there is a better approach to avoid these kind of problems, so you can see our approach for avoiding the above problems.
回答1:
After hours of brainstorming we decided to use four basic services from AWS
- AWS API Gateway
- AWS Lambda function
- AWS SNS (Simple Notification Service)
- AWS Cloudwatch for monitoring and logging.
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.
The architecture looks as follow
Detailed view
Simple view
Explanation
We are going to describe the process explaining step by step of the flow to deliver a SMS.
- A source needs to send a message to a specific telephone number, so the caller execute a POST request (/delivermessage) with the following payload to the API Gateway endpoing
{
"target": "554542121245",
"type": "sms",
"message": "Hello World!",
"region": "us-east-1"
}
- The API Gateway validates the API to grant access and send the received payload to the Lambda function.
The Lambda function validates the received payload and execute the following:
- Creates a SNS topic.
- Creates a subscription using the received telephone number.
- Subscribes it to the topic.
- Publishes the message through that subscription.
- Removes subscription.
- Removes topic.
- Returns back a success response to the caller:
{
"status": 200,
"message": "The message has been sent!"
}
- The API Gateway evaluates the response and send back the response to the caller.
- The API Gateway has intelligence to check what kind of response was sent from the Lambda function.
- For response starts with 412 means Precondition Failed.
- For response starts with 500 means Internal server error.
Lambda Code (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 template
This cloudformation template describes the whole set of services, API Gateway, Lambda function, Roles, Permissions, Usage plans for the API, API Key, Etc.
For downloading click 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
Hope it helps to anybody who needs to deploy a microservice to send SMS.
来源:https://stackoverflow.com/questions/48194394/is-there-a-best-approach-to-deploy-an-architecture-to-send-sms-using-a-microserv