是否有部署架构以使用微服务模型发送 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'
}
};
如果我们需要发送短信,只需调用此服务即可。
这里不好的是下面的,我们知道的:
我们在 EC2 中使用密钥。但是,我们正在努力设置一个对实例具有特定权限的角色。
假设我们需要修改发送短信的方式,我们将不得不为应用程序的那一小部分重新部署整个应用程序。
最糟糕的是,假设我们的应用程序在 AutoScaling 上。我们将不得不放弃所有实例,只是为了更新我们应用程序的那一小部分。
另一个问题是,如果我们必须在其他应用程序中使用该服务怎么办?当前的方法导致在应用程序之间重复服务。
最后,如何记录,监控等
我们认为有更好的方法可以避免此类问题,所以您可以查看我们避免上述问题的方法。
经过几个小时的头脑风暴,我们决定使用 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 的流程。
- 源需要向特定的电话phone号码发送消息,因此调用方执行POST请求(/delivermessage),并向API网关发送以下有效负载终点
{
"target": "554542121245",
"type": "sms",
"message": "Hello World!",
"region": "us-east-1"
}
API 网关验证 API 以授予访问权限并将接收到的负载发送到 Lambda 函数。
Lambda 函数验证接收到的负载并执行以下操作:
- 创建 SNS 主题。
- 使用收到的电话号码创建订阅phone。
- 订阅主题。
- 通过该订阅发布消息。
- 删除订阅。
- 删除主题。
- Returns 向调用方返回成功响应:
{
"status": 200,
"message": "The message has been sent!"
}
- 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"
}
我们在后端 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'
}
};
如果我们需要发送短信,只需调用此服务即可。
这里不好的是下面的,我们知道的:
我们在 EC2 中使用密钥。但是,我们正在努力设置一个对实例具有特定权限的角色。
假设我们需要修改发送短信的方式,我们将不得不为应用程序的那一小部分重新部署整个应用程序。
最糟糕的是,假设我们的应用程序在 AutoScaling 上。我们将不得不放弃所有实例,只是为了更新我们应用程序的那一小部分。
另一个问题是,如果我们必须在其他应用程序中使用该服务怎么办?当前的方法导致在应用程序之间重复服务。
最后,如何记录,监控等
我们认为有更好的方法可以避免此类问题,所以您可以查看我们避免上述问题的方法。
经过几个小时的头脑风暴,我们决定使用 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 的流程。
- 源需要向特定的电话phone号码发送消息,因此调用方执行POST请求(/delivermessage),并向API网关发送以下有效负载终点
{
"target": "554542121245",
"type": "sms",
"message": "Hello World!",
"region": "us-east-1"
}
API 网关验证 API 以授予访问权限并将接收到的负载发送到 Lambda 函数。
Lambda 函数验证接收到的负载并执行以下操作:
- 创建 SNS 主题。
- 使用收到的电话号码创建订阅phone。
- 订阅主题。
- 通过该订阅发布消息。
- 删除订阅。
- 删除主题。
- Returns 向调用方返回成功响应:
{
"status": 200,
"message": "The message has been sent!"
}
- 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"
}