After hours of brainstorming we decided to use four basic services from 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.
##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

Update - 2021
The SNS js sdk provides a way of sending sms directly without the need of creating topics.