Develop a Load Balancer using AWS Lambda


Why Lambda?

AWS Lambda is serverless and low costs. We don’t need to maintain (upgrade, patch) the server. Lambda API costs less than spawing up a EC2 instance. Lambda by natual is highly scalable. And Serverless is one of the most secure way of building the cloud.

The major Cloud Proviers have serverless functions such as AWS Lambda, Azure Function, CloudFlare Worker.

What is a Load Balancer?

A Load Balancer (LB) routes the traffic to different servers to avoid single point of failure or overloading a particular server. A LB could be a server, or coud be simply DNS configured. AWS has Application Load Balancer, Elastic Load Balancer and Network Load Balancer. A LB is usually used to scale the server reads horizontally, so you can add as many servers behind a Load Balancer as you like to meet the increasing traffic.

load-balancer-resolves-to-different-web-servers Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

Previously, we use CloudFlare Worker (another Serverless Function) to route the traffic to the fastest Steem API Node. The Load Balancer is implemented in Node Js.

load-balancer-1024x437 Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

load-balancer

LB routing algorithms can be based on least-load, round-robin, fastest pings, or even simply randomness.

Setting up a Load Balancer using AWS Lambda

We can use a serverless function to provide a Load Balancer API – then, for each request hitting the Serverless API i.e. Lambda, who sends a request simutanenously to all the candidate API servers and choose the fastest healthy server to forward the request to. When the candidate returns JSON result, Lambda also adds some meta information on it before forwarding back to the requester.

This is by far the simplest Load Balancer that does not rely on checking and maintaining up-to-date servers’ healthy status. We can also create a canary that periodically sends PING requests to the candidate servers behind the LB and record the healthy status. And one Lambda LB gets the request, it will look for the least load (or most healthy one) to serve the request.

aws-lambda-upload-interface Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

Open the Lambda Source Code Editor, we need to import the node-fetch library. And let’s define promiseAny function that returns a promise of the first successful request.

1
2
3
4
5
6
7
8
9
const fetch = require('node-fetch');
 
function reverse(promise) {
    return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve));
}
 
function promiseAny(iterable) {
    return reverse(Promise.all([...iterable].map(reverse)));
};
const fetch = require('node-fetch');

function reverse(promise) {
    return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve));
}

function promiseAny(iterable) {
    return reverse(Promise.all([...iterable].map(reverse)));
};

The shuffle function is to used to shuffle the array of the list of the candidate servers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function shuffle(array) {
  let currentIndex = array.length, temporaryValue, randomIndex;
 
  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
 
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
 
    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
 
  return array;
}
function shuffle(array) {
  let currentIndex = array.length, temporaryValue, randomIndex;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

Let’s define a list of the candidate servers i.e. Steem Blockchain API nodes.

1
2
3
4
5
let nodes = shuffle([
  "https://api.steemit.com",
  "https://api.justyy.com",
  "https://api.steemitdev.com",
]);
let nodes = shuffle([
  "https://api.steemit.com",
  "https://api.justyy.com",
  "https://api.steemitdev.com",
]);

A function to call the API of Steem Node to return the API version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getVersion(server) {
  return new Promise((resolve, reject) => {
    fetch(server, {
      method: "POST",
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({"id":0,"jsonrpc":"2.0","method":"call","params":["login_api","get_version",[]]})
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}
async function getVersion(server) {
  return new Promise((resolve, reject) => {
    fetch(server, {
      method: "POST",
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({"id":0,"jsonrpc":"2.0","method":"call","params":["login_api","get_version",[]]})
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}

The following function pings the server and return the response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function contactServer(server) {
  return new Promise((resolve, reject) => {
    fetch(server, {
      method: "GET"
    }).then(response => {
      if (!response.ok) {
        reject({
            "server": "", 
            "error": response, 
            "host": server
        });
      } else {
        resolve({
          "server": server,
        });
      }
    }).catch(function(error) {
      reject({
          "server": "", 
          "error": error, 
          "host": server
      });
    });
  });
}
async function contactServer(server) {
  return new Promise((resolve, reject) => {
    fetch(server, {
      method: "GET"
    }).then(response => {
      if (!response.ok) {
        reject({
            "server": "", 
            "error": response, 
            "host": server
        });
      } else {
        resolve({
          "server": server,
        });
      }
    }).catch(function(error) {
      reject({
          "server": "", 
          "error": error, 
          "host": server
      });
    });
  });
}

Forwarding the GET Request to the winning server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function forwardRequestGET(apiURL) {
  return new Promise((resolve, reject) => {
    fetch(apiURL, {
      method: "GET",   
      headers: {
        'Content-Type': 'application/json'
      },
      redirect: "follow"
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}
async function forwardRequestGET(apiURL) {
  return new Promise((resolve, reject) => {
    fetch(apiURL, {
      method: "GET",   
      headers: {
        'Content-Type': 'application/json'
      },
      redirect: "follow"
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}

Forwarding the POST request with body (parameters):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function forwardRequestPOST(apiURL, body) {
  return new Promise((resolve, reject) => {
    fetch(apiURL, {
      method: "POST",   
      redirect: "follow",
      headers: {
      'Content-Type': 'application/json'
      },      
      body: body
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}
async function forwardRequestPOST(apiURL, body) {
  return new Promise((resolve, reject) => {
    fetch(apiURL, {
      method: "POST",   
      redirect: "follow",
      headers: {
      'Content-Type': 'application/json'
      },      
      body: body
    }).then(response => {
      resolve(response.text());
    }).catch(function(error) {
      reject(error);
    });
  });
}

The main entry of the Lambda handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
exports.handler = async(event, context) => {
  const servers = [];
  nodes = shuffle(nodes);
  for (const server of nodes) {
    servers.push(contactServer(server));
  }
  const load = await promiseAny(servers); 
  const forwardedURL = load['server'];
  let method = "POST";
  if (event["http-method"]) {
      method = event["http-method"].toUpperCase();
  }
  let result;
  let res;
  let version = "";
  try {
    version = await getVersion(load['server']);
  } catch (e) {
    version = JSON.stringify(e);
  }
  console.log("server = ", load['server']);
  console.log("version = ", version);
  let body = {};
  if (event["body-json"]) {
    body = event["body-json"];
    body = JSON.stringify(body);
    if (body.length <= 2) {
      method = "GET";
    }
  } else {
    method = "GET";
  }
  try {    
    if (method === "POST") {
        result = await forwardRequestPOST(forwardedURL, body);
    } else if (method === "GET") {
        result = await forwardRequestGET(forwardedURL);
    } else {
        return {
            statusCode: 405,
            statusText: 'Method Not Allowed',
            headers: {
                'Access-Control-Allow-Origin': '*',
                'Cache-Control': 'max-age=3600',
            }
        };
    }
  } catch (e) {
    return {
        statusCode: 500,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
            'Cache-Control': 'max-age=3',
            'Version': version,
            'Error': JSON.stringify(e)
        }
    };
  }
  // Adding meta information before forwarding to requester.
  result = JSON.parse(result);
  try {
    result["__version__"] = JSON.parse(version);
  } catch (e) {
    result["__version__"] = version;
  }
  result["__server__"] = load['server'];
  return result;
};
exports.handler = async(event, context) => {
  const servers = [];
  nodes = shuffle(nodes);
  for (const server of nodes) {
    servers.push(contactServer(server));
  }
  const load = await promiseAny(servers); 
  const forwardedURL = load['server'];
  let method = "POST";
  if (event["http-method"]) {
      method = event["http-method"].toUpperCase();
  }
  let result;
  let res;
  let version = "";
  try {
    version = await getVersion(load['server']);
  } catch (e) {
    version = JSON.stringify(e);
  }
  console.log("server = ", load['server']);
  console.log("version = ", version);
  let body = {};
  if (event["body-json"]) {
    body = event["body-json"];
    body = JSON.stringify(body);
    if (body.length <= 2) {
      method = "GET";
    }
  } else {
    method = "GET";
  }
  try {    
    if (method === "POST") {
        result = await forwardRequestPOST(forwardedURL, body);
    } else if (method === "GET") {
        result = await forwardRequestGET(forwardedURL);
    } else {
        return {
            statusCode: 405,
            statusText: 'Method Not Allowed',
            headers: {
                'Access-Control-Allow-Origin': '*',
                'Cache-Control': 'max-age=3600',
            }
        };
    }
  } catch (e) {
    return {
        statusCode: 500,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
            'Cache-Control': 'max-age=3',
            'Version': version,
            'Error': JSON.stringify(e)
        }
    };
  }
  // Adding meta information before forwarding to requester.
  result = JSON.parse(result);
  try {
    result["__version__"] = JSON.parse(version);
  } catch (e) {
    result["__version__"] = version;
  }
  result["__server__"] = load['server'];
  return result;
};

AWS API Gateway

With Lambda, it is not enough. We have to set up a API Gateway that uses the Lambda. Also, in Integration Request, we have to add a mapping template for “application/json” that passes some variables e.g. http-method to Lambda.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
##  See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
##  This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload
#set($allParams = $input.params())
{
"body-json" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
"$type" : {
    #foreach($paramName in $params.keySet())
    "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
        #if($foreach.hasNext),#end
    #end
}
    #if($foreach.hasNext),#end
#end
},
"stage-variables" : {
#foreach($key in $stageVariables.keySet())
"$key" : "$util.escapeJavaScript($stageVariables.get($key))"
    #if($foreach.hasNext),#end
#end
},
"context" : {
    "account-id" : "$context.identity.accountId",
    "api-id" : "$context.apiId",
    "api-key" : "$context.identity.apiKey",
    "authorizer-principal-id" : "$context.authorizer.principalId",
    "caller" : "$context.identity.caller",
    "cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
    "cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
    "cognito-identity-id" : "$context.identity.cognitoIdentityId",
    "cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
    "http-method" : "$context.httpMethod",
    "stage" : "$context.stage",
    "source-ip" : "$context.identity.sourceIp",
    "user" : "$context.identity.user",
    "user-agent" : "$context.identity.userAgent",
    "user-arn" : "$context.identity.userArn",
    "request-id" : "$context.requestId",
    "resource-id" : "$context.resourceId",
    "resource-path" : "$context.resourcePath"
    }
}
##  See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
##  This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload
#set($allParams = $input.params())
{
"body-json" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
"$type" : {
    #foreach($paramName in $params.keySet())
    "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
        #if($foreach.hasNext),#end
    #end
}
    #if($foreach.hasNext),#end
#end
},
"stage-variables" : {
#foreach($key in $stageVariables.keySet())
"$key" : "$util.escapeJavaScript($stageVariables.get($key))"
    #if($foreach.hasNext),#end
#end
},
"context" : {
    "account-id" : "$context.identity.accountId",
    "api-id" : "$context.apiId",
    "api-key" : "$context.identity.apiKey",
    "authorizer-principal-id" : "$context.authorizer.principalId",
    "caller" : "$context.identity.caller",
    "cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
    "cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
    "cognito-identity-id" : "$context.identity.cognitoIdentityId",
    "cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
    "http-method" : "$context.httpMethod",
    "stage" : "$context.stage",
    "source-ip" : "$context.identity.sourceIp",
    "user" : "$context.identity.user",
    "user-agent" : "$context.identity.userAgent",
    "user-arn" : "$context.identity.userArn",
    "request-id" : "$context.requestId",
    "resource-id" : "$context.resourceId",
    "resource-path" : "$context.resourcePath"
    }
}

amazon-api-gateway-lambda-1024x583 Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

Cost Saving using Lambda compared to EC2 instance

Compared to AWS Free Tier t2.micro, the cost saving is significant. The Lambda costs around 0.05 USD per day that is 1.5 USD per month. And I don’t need to worry about the maintanence or patching the server. And since it is serverless – it is highly scablable as well. I would assume it is highly available compared to EC2 instances.

aws-cost-management-lamba Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

The Monitor/Meterics show the Load Balancer made in Serverless Lambda works as expected.
lambda-metrics-monitor-1024x866 Develop a Load Balancer using AWS Lambda amazon Amazon Web Services cloud cloud computing Load Balancing nodejs

–EOF (The Ultimate Computing & Technology Blog) —

GD Star Rating
loading...
1501 words
Last Post: Teaching Kids Programming - Dynamic Programming Algorithm to Compute the Triangle Minimum Path Sum
Next Post: Algorithm to Find the Longest Anagram Subsequence

The Permanent URL is: Develop a Load Balancer using AWS Lambda

Leave a Reply