From the beginning, Mitoc Group
has been building web applications for enterprise customers. We are a
small group of developers who are helping customers with their entire
web development process, from conception through execution and down to
maintenance. Being in the business of doing everything is very hard, and
it would be impossible without using AWS foundational services, but we
incrementally needed more. That is why we became early adopters of the
serverless computing approach and developed an ecosystem called Digital Enterprise End-to-end Platform (DEEP) with AWS Lambda at the core.
In this post, we dive deeper into how DEEP is using AWS Lambda to empower developers to build cloud-native applications or platforms using microservices architecture. We will walk you through the process of identifying the front-end, back-end and data tiers required to build web applications with AWS Lambda at the core. We will focus on the structure of the AWS Lambda functions we use, as well as security, performance and benchmarking steps that we take to build enterprise-level web applications.
Nevertheless, what we have learned from our customers is that enterprise-level web applications must provide the following common expectations:
The architecture of every web application we build or transform, including the one described above, is similar to the reference architecture of the realtime voting application published recently by AWS on GitHub.
The todo app is written in AngularJS and deployed on Amazon S3, behind Amazon CloudFront (front-end). Task management is processed by AWS Lambda, optionally behind Amazon API Gateway (back-end). Task metadata is stored in Amazon DynamoDB (data tier). The transformed todo app, along with instructions on how to install and deploy this web application, is described in the Building Scalable Web Apps with AWS Lambda and Home-Grown Serverless blog post and the todo code is available on GitHub.
Let’s look at AWS Lambda functions and the value proposition they offer to us and our customers.
Therefore, coming back to AWS Lambda, we have written four small Node.js functions that are context-bounded and self-sustained (each microservice corresponds to the above identified back-end web service):
Each above file with related dependencies is compressed
into .zip file and uploaded to AWS Lambda. If you’re new to this
process, we strongly recommend following the How to Create, Upload and Invoke an AWS Lambda function tutorial.
Back to the four small Node.js functions, you can see that we have adopted ES6 (aka ES2015) as our coding standard. And we are importing deep-framework in every function. What is this framework anyway and why are we using it everywhere?
We believe that the only way to achieve this kind of flexibility and scale is automation and code reuse. These principles led us to build and open source DEEP Framework — a full-stack web framework that abstracts web services and web applications from specific cloud services — and DEEP CLI (aka deepify) — a development tool-chain that abstracts package management and associated development operations.
Therefore, to make sure that the process of managing AWS Lambda functions is streamlined and automated, we consistently include two more files in each uploaded .zip:
Having these three files (Handler.es6, bootstrap.es6, and
package.json) in each Lambda function doesn’t mean that your final .zip
file will be that small. Actually, a lot of additional operations happen
before the .zip file is created. To name a few:
Second, install the DEEP CLI with the following command:
Next, deploy the todo app using deepify:
Note: When the deepify server command is finished, you can open http://localhost:8000 in your browser and enjoy the todo app running locally.
As you can see, we empower developers to build hassle-free, cloud-native applications or platforms using microservices architecture and serverless computing.
And what about security?
End users benefit from IAM best practices through streamlined implementations of least privilege access, delegated roles instead of credentials, and integration with logging and monitoring services (e.g., AWS CloudTrail, Amazon CloudWatch, and Amazon Elasticsearch Service + Kibana). For example, developers and end users of the todo app didn’t need to explicitly define any security roles (it was done by deepify deploy), but they can rest assured that only their instance of todo app will be using their infrastructure, platform, and application resources.
The following are two security roles (back-end and front-end) that have been seamlessly generated and enforced in each layer:
Particularly, for todo app, we performed various benchmarking analysis on AWS Lambda by tweaking different components in a specific function (e.g. function size, memory size, billable cost, etc.). Next, we would like to share results with you:
Using the benchmarking tool, we ran multiple scenarios on the same function from todo app
Based on performance data, we have learned some pretty cool stuff:
If you have questions or suggestions, please leave a comment below.
In this post, we dive deeper into how DEEP is using AWS Lambda to empower developers to build cloud-native applications or platforms using microservices architecture. We will walk you through the process of identifying the front-end, back-end and data tiers required to build web applications with AWS Lambda at the core. We will focus on the structure of the AWS Lambda functions we use, as well as security, performance and benchmarking steps that we take to build enterprise-level web applications.
Enterprise-level web applications
Our approach to web development is full-stack and user-driven, focused on UI (aka the user interface) and UX (aka user eXperience). Before going into the details, we’d like to emphasize the strategical (biased and opinionated) decisions we made early on:- We don’t say “no” to customers; every problem is seriously evaluated and sometimes we offer options that involve our direct competitors.
- We are developers and we focus only on the application level; everything else (platform level and infrastructure level) must be managed by AWS.
- We focus 20% of effort to solve 80% of work load; everything must be automated and pushed on the service side rather than the client side.
Nevertheless, what we have learned from our customers is that enterprise-level web applications must provide the following common expectations:
- Be secure — security through obscurity (e.g., Amazon IAM, Amazon Cognito)
- Be compliant — governance-focused, audit-friendly service features with applicable compliance or audit standards
- Be reliable — service level agreements (e.g. Amazon S3, Amazon CloudFront)
- Be performant — studies show that page loads longer than 2s start impacting the users behavior
- Be pluggable — successful enterprise ecosystem is mainly driven by fully integrated web applications inside organizations;
- Be cost-efficient — a benefit of the AWS Free Tier, as well as pay only for services that you use and when you use them
- Be scalable — the serverless approach relies on abstracted services that are pre-scaled to AWS size, whatever that would be
Architecture
This post describes how we transformed a self-managed task management application (aka todo app) in minutes. The original version can be seen on www.todomvc.com and the original code can be downloaded from https://github.com/tastejs/todomvc/tree/master/examples/angularjs.The architecture of every web application we build or transform, including the one described above, is similar to the reference architecture of the realtime voting application published recently by AWS on GitHub.
The todo app is written in AngularJS and deployed on Amazon S3, behind Amazon CloudFront (front-end). Task management is processed by AWS Lambda, optionally behind Amazon API Gateway (back-end). Task metadata is stored in Amazon DynamoDB (data tier). The transformed todo app, along with instructions on how to install and deploy this web application, is described in the Building Scalable Web Apps with AWS Lambda and Home-Grown Serverless blog post and the todo code is available on GitHub.
Let’s look at AWS Lambda functions and the value proposition they offer to us and our customers.
AWS Lambda functions
The goal of the todo app is to manage tasks in a self-service mode. End users can view tasks, create new tasks, mark or unmark a task as done, and clear completed tasks. From the UI point of view, that leads to four user interactions that require different back-end calls:- web service that retrieves tasks
- web service that creates tasks
- web service that deletes tasks
- web service that updates tasks
Therefore, coming back to AWS Lambda, we have written four small Node.js functions that are context-bounded and self-sustained (each microservice corresponds to the above identified back-end web service):
Microservice that retrieves tasks
JavaScript
'use strict';
import DeepFramework from 'deep-framework';
export default class Handler extends DeepFramework.Core.AWS.Lambda.Runtime {
/**
* @param {Array} args
*/
constructor(...args) {
super(...args);
}
/**
* @param request
*/
handle(request) {
let taskId = request.getParam('Id');
if (taskId) {
this.retrieveTask(taskId, (task) => {
return this.createResponse(task).send();
});
} else {
this.retrieveAllTasks((result) => {
return this.createResponse(result).send();
});
}
}
/**
* @param {Function} callback
*/
retrieveAllTasks(callback) {
let TaskModel = this.kernel.get('db').get('Task');
TaskModel.findAll((err, task) => {
if (err) {
throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
}
return callback(task.Items);
});
}
/**
* @param {String} taskId
* @param {Function} callback
*/
retrieveTask(taskId, callback) {
let TaskModel = this.kernel.get('db').get('Task');
TaskModel.findOneById(taskId, (err, task) => {
if (err) {
throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
}
return callback(task ? task.get() : null);
});
}
}
Microservice that creates a task
JavaScript
'use strict';
import DeepFramework from 'deep-framework';
export default class extends DeepFramework.Core.AWS.Lambda.Runtime {
/**
* @param {Array} args
*/
constructor(...args) {
super(...args);
}
/**
* @param request
*/
handle(request) {
let TaskModel = this.kernel.get('db').get('Task');
TaskModel.createItem(request.data, (err, task) => {
if (err) {
throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
}
return this.createResponse(task.get()).send();
});
}
}
Microservice that updates a task
JavaScript
'use strict';
import DeepFramework from 'deep-framework';
export default class Handler extends DeepFramework.Core.AWS.Lambda.Runtime {
/**
* @param {Array} args
*/
constructor(...args) {
super(...args);
}
/**
* @param request
*/
handle(request) {
let taskId = request.getParam('Id');
if (typeof taskId !== 'string') {
throw new InvalidArgumentException(taskId, 'string');
}
let TaskModel = this.kernel.get('db').get('Task');
TaskModel.updateItem(taskId, request.data, (err, task) => {
if (err) {
throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
}
return this.createResponse(task.get()).send();
});
}
}
Microservice that deletes a task
JavaScript
'use strict';
import DeepFramework from 'deep-framework';
export default class extends DeepFramework.Core.AWS.Lambda.Runtime {
/**
* @param {Array} args
*/
constructor(...args) {
super(...args);
}
/**
* @param request
*/
handle(request) {
let taskId = request.getParam('Id');
if (typeof taskId !== 'string') {
throw new DeepFramework.Core.Exception.InvalidArgumentException(taskId, 'string');
}
let TaskModel = this.kernel.get('db').get('Task');
TaskModel.deleteById(taskId, (err) => {
if (err) {
throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
}
return this.createResponse({}).send();
});
}
}
Back to the four small Node.js functions, you can see that we have adopted ES6 (aka ES2015) as our coding standard. And we are importing deep-framework in every function. What is this framework anyway and why are we using it everywhere?
Full-stack web framework
Step back for a minute. Building and uploading AWS Lambda functions to the service is very simple and straight-forward, but now imagine that you need to manage 100–150 web services to access a web page, multiplied by hundreds or thousands of web pages.We believe that the only way to achieve this kind of flexibility and scale is automation and code reuse. These principles led us to build and open source DEEP Framework — a full-stack web framework that abstracts web services and web applications from specific cloud services — and DEEP CLI (aka deepify) — a development tool-chain that abstracts package management and associated development operations.
Therefore, to make sure that the process of managing AWS Lambda functions is streamlined and automated, we consistently include two more files in each uploaded .zip:
DEEP microservice bootstrap
JavaScript
'use strict';
import DeepFramework from 'deep-framework';
import Handler from './Handler';
export default DeepFramework.LambdaHandler(Handler);
DEEP microservice package metadata (for npm)
JavaScript
{
"name": "deep-todo-task-create",
"version": "0.0.1",
"description": "Create a new todo task",
"scripts": {
"postinstall": "npm run compile",
"compile": "deepify compile-es6 `pwd`"
},
"dependencies": {
"deep-framework": "^1.8.x"
},
"preferGlobal": false,
"private": true,
"analyze": true
}
- AWS Lambda performs better when the uploaded codebase is smaller. Because we provide both local development capabilities and one-step push to production, our process optimizes resources before deploying to AWS.
- ES6 is not supported by the node.js v0.10.x runtime that we use in AWS Lambda, it is however available in the Node 4.3 runtime, so we compile .es6 files into ES5-compliant .js files using Babel.
- Dependencies that are defined in package.json are automatically pulled and fine-tuned for node.js v0.10.x to provide the best performance possible.
Putting everything together
First, you need the following pre-requisites:- AWS account (Create an Amazon Web Services Account)
- AWS CLI (Configure AWS Command Line Interface)
- Git v2+ (Get Started — Installing Git)
- Java / JRE v6+ (JDK 8 and JRE 8 Installation Start Here)
- js v4+ (Install nvm and Use latest node v4)
Second, install the DEEP CLI with the following command:
npm install deepify -g
Next, deploy the todo app using deepify:
deepify install github://MitocGroup/deep-microservices-todo-app ~/deep-todo-app
deepify server ~/deep-todo-app
deepify deploy ~/deep-todo-app
Note: When the deepify server command is finished, you can open http://localhost:8000 in your browser and enjoy the todo app running locally.
Cleaning up
There are at least half a dozen services and several dozen of resources created during deepify deploy. If only there was a simple command that would clean up everything when we’re done. We thought of that and created deepify undeploy to address this need. When you are done using todo app and want to remove web app related resources, execute the following:deepify undeploy ~/deep-todo-app
As you can see, we empower developers to build hassle-free, cloud-native applications or platforms using microservices architecture and serverless computing.
And what about security?
Security
One of the biggest value propositions on AWS is out-of-the-box security and compliance. The beauty of the cloud-native approach is that security comes by design (in other words, it won’t work otherwise). We take full advantage of that shared responsibility model and enforce security in every layer.End users benefit from IAM best practices through streamlined implementations of least privilege access, delegated roles instead of credentials, and integration with logging and monitoring services (e.g., AWS CloudTrail, Amazon CloudWatch, and Amazon Elasticsearch Service + Kibana). For example, developers and end users of the todo app didn’t need to explicitly define any security roles (it was done by deepify deploy), but they can rest assured that only their instance of todo app will be using their infrastructure, platform, and application resources.
The following are two security roles (back-end and front-end) that have been seamlessly generated and enforced in each layer:
IAM role that allows back-end invocation of AWS Lambda function (e.g. DeepProdTodoCreate1234abcd) in web application AWS account (e.g. 123456789000)
JavaScript
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["lambda:InvokeFunction"],
"Resource": ["arn:aws:lambda:us-east-1:123456789000:function:DeepProdTodoCreate1234abcd*"]
}
]
}
DEEP role that allows front-end resource (e.g deep.todo:task) to execute action (e.g. deep.todo:task:create)
JavaScript
{
"Version": "2015-10-07",
"Statement": [
{
"Effect": "Allow",
"Action": ["deep.todo:task:create"],
"Resource": ["deep.todo:task"]
}
]
}
Benchmarking
We have been continuously benchmarking AWS Lambda for various use cases in our microapplications. After a couple of repetitive situations doing similar analysis, we decided to build the benchmarking as another microapplication and re-use the ecosystem to include it automatically where we needed it. You can find the open-source code for the benchmarking microapplication on GitHub:Particularly, for todo app, we performed various benchmarking analysis on AWS Lambda by tweaking different components in a specific function (e.g. function size, memory size, billable cost, etc.). Next, we would like to share results with you:
Benchmarking for todo app
Req No | Function Size (MB) | Memory Size (MB) | Max Memory Used (MB) | Start time | Stop time | Front-end Call (ms) | Back-end Call (ms) | Billed Time (ms) | Billed Time ($) |
---|---|---|---|---|---|---|---|---|---|
1 | 1.1 | 128 | 34 | 20:15.8 | 20:16.2 | 359 | 200.47 | 300 | 0.000000624 |
2 | 1.1 | 128 | 34 | 20:17.8 | 20:18.2 | 381 | 202.45 | 300 | 0.000000624 |
3 | 1.1 | 128 | 34 | 20:19.9 | 20:20.3 | 406 | 192.52 | 200 | 0.000000416 |
4 | 1.1 | 128 | 34 | 20:21.9 | 20:22.2 | 306 | 152.19 | 200 | 0.000000416 |
5 | 1.1 | 128 | 34 | 20:23.9 | 20:24.2 | 333 | 175.01 | 200 | 0.000000416 |
6 | 1.1 | 128 | 34 | 20:25.9 | 20:26.3 | 431 | 278.03 | 300 | 0.000000624 |
7 | 1.1 | 128 | 34 | 20:27.9 | 20:28.2 | 323 | 170.97 | 200 | 0.000000416 |
8 | 1.1 | 128 | 34 | 20:29.9 | 20:30.2 | 327 | 160.24 | 200 | 0.000000416 |
9 | 1.1 | 128 | 34 | 20:31.9 | 20:32.4 | 556 | 225.25 | 300 | 0.000000624 |
10 | 1.1 | 128 | 35 | 20:33.9 | 20:34.2 | 333 | 179.59 | 200 | 0.000000416 |
Average | 375.50 | 193.67 | Total | 0.000004992 |
Performance
Speaking of performance, we find AWS Lambda mature enough to power large-scale web applications. The key is to build the functions as small as possible, focusing on a simple rule of one function to achieve only one task. Over time, these functions might grow in size; therefore, we always keep an eye on them and re-factor / split into the lowest possible logical denominator (smallest task).Using the benchmarking tool, we ran multiple scenarios on the same function from todo app
Function Size (MB) | Memory Size (MB) | Max Memory Used (MB) | Avg Front-end (ms) | Avg Back-end (ms) | Total Calls (#) | Total Billed (ms) | Total Billed ($/1B)* |
---|---|---|---|---|---|---|---|
1.1 | 128 | 34-35 | 375.50 | 193.67 | 10 | 2,400 | 4,992 |
1.1 | 256 | 34-37 | 399.40 | 153.25 | 10 | 2,000 | 8,340 |
1.1 | 512 | 33-35 | 341.60 | 134.32 | 10 | 1,800 | 15,012 |
1.1 | 128 | 34-49 | 405.57 | 223.82 | 100 | 27,300 | 56,784 |
1.1 | 256 | 28-48 | 354.75 | 177.91 | 100 | 23,800 | 99,246 |
1.1 | 512 | 32-47 | 345.92 | 163.17 | 100 | 23,100 | 192,654 |
55.8 | 128 | 49-50 | 543.00 | 284.03 | 10 | 3,400 | 7,072 |
55.8 | 256 | 49-50 | 339.80 | 153.13 | 10 | 2,100 | 8,757 |
55.8 | 512 | 49-50 | 342.60 | 141.02 | 10 | 2,000 | 16,680 |
55.8 | 128 | 83-87 | 416.10 | 220.91 | 100 | 26,900 | 55,952 |
55.8 | 256 | 50-71 | 377.69 | 194.22 | 100 | 25,600 | 106,752 |
55.8 | 512 | 57-81 | 353.46 | 174.65 | 100 | 23,300 | 194,322 |
- The smaller the function is, the better it performs; On the other hand, if more memory is allocated, the size of the function matters less and less.
- Memory size is not directly proportional to billable costs; developers can decide the memory size based on performance requirements combined with associated costs.
- The key to better performance is continuous load, thanks to container reuse in AWS Lambda.
Conclusion
In this post, we presented a small web application that is built with AWS Lambda at the core. We walked you through the process of identifying the front-end, back-end, and data tiers required to build the todo app. You can fork the example code repository as a starting point for your own web applications.If you have questions or suggestions, please leave a comment below.
0 comments:
Post a Comment