Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a best approach to deploy an architecture to send SMS using a Microservice model?

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.

like image 852
Ele Avatar asked Jan 29 '23 21:01

Ele


2 Answers

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 enter image description here


###Simple view

enter image description here


#Explanation

We are going to describe the process explaining step by step of the flow to deliver a SMS.

  1. 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"
}

  1. The API Gateway validates the API to grant access and send the received payload to the Lambda function.

  2. 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!"
}
           

  1. 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

enter image description here

Update - 2021

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

like image 114
Ele Avatar answered Feb 05 '23 18:02

Ele


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"
}
like image 29
Manuel Bichler Avatar answered Feb 05 '23 17:02

Manuel Bichler