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.
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.
{
"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:
{
"status": 200,
"message": "The message has been sent!"
}
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}`;
}
};
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
The SNS js sdk provides a way of sending sms directly without the need of creating topics.
If your use case is to deliver single sms to individuals then you don't need to create a topic and delete it afterwards. It's possible to simply send one sms with the following code.
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;
}
});
};
the api endpoint/lambda receives the following body
{
"message": "hey ho I am the sms message.",
"messageType": "Transactional", //or "Promotional"
"messageSender": "Your Brand",
"phone":"+436640339333"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With