在 LinuxCon/ContainerCon 2015 会议上,我做了一个以演示驱动,名为 Microservices without the Servers (《无服务器微服务》)的演讲。在这篇演讲中,我创建了一项图像处理微服务,将它部署到多个区域,构建了一个以该服务为后端的移动应用,使用Amazon API Gateway和一个网站添加了一个基于HTTPS的API,然后对其进行单元和负载测试,所有这些都没有使用任何服务器。
本博客详细地再现了这篇演讲,带你逐步浏览每一步所必需的细节,更深入地了解这一架构。想要查看高层次的概述,参阅 胶片 。想要查看该架构的另一个实例,查看可执行要点库, SquirrelBin 。
无服务器架构
“无服务器”,我们是指不需要显式的基础设施,就像无服务器,无服务器上的部署,无任何种类的软件安装。我们将仅使用托管的云服务和一个笔记本电脑。下面的图表阐明了这一架构的高级组件和它们之间的连接:一个Lambda函数能作为计算(“后端”)和一个直接连接到该函数的移动应用,加上Amazon API Gateway为一个静态的Amazon S3承载的网站提供一个HTTP端点。
适用于使用AWS Lambda函数的移动和Web应用的无服务器架构
现在就让我们开始构建这一架构吧!
第一步:创建图像处理服务
为了使这一过程比较容易实施,我们将使用一个库,该库是一个内置库,使用Lambda的nodejs语言,ImageMagick。但是,这不是必须的–如果你更喜欢用自己的库,你可以 加载JavaScript脚本或本地库 , 运行Python ,或甚至 包装一个可执行的命令行 。下面的示例是用nodejs语言实施的,但是你也可以使用Java,Clojure,Scala,或AWS Lambda中其他的基于jvm的语言构建该服务。
下面的代码是一种适用于ImageMagick的“hello world”程序–它给了我们一个基本的命令结构(又叫做,一个切换语句),使我们能提取并返回内置的玫瑰图像。除了将结果进行编码所以它能以JSON格式快乐地存在外,其他没有什么。
var im = require("imagemagick"); var fs = require("fs"); exports.handler = function(event, context) { if (event.operation) console.log("Operation " + event.operation + " requested"); switch (event.operation) { case 'ping': context.succeed('pong'); return; case 'getSample': event.customArgs = ["rose:", "/tmp/rose.png"]; im.convert(event.customArgs, function(err, output) { if (err) context.fail(err); else { var resultImgBase64 = new Buffer(fs.readFileSync("/tmp/rose.png")).toString('base64'); try {fs.unlinkSync("/tmp/rose.png");} catch (e) {} // discard context.succeed(resultImgBase64); } }); break; // allow callback to complete default: var error = new Error('Unrecognized operation "' + event.operation + '"'); context.fail(error); return; } };
首先,让我们在AWS Lambda控制台的测试窗口中给它发送如下JSON命令确保服务正在运行:
{ "operation": "ping" }
你应该能获得必不可少的“pong”响应。下一步,我们发送与下面的内容相类似的JSON命令来实际调用ImageMagick:
{ "operation": "getSample" }
该请求提取一个base64编码的字符串,该字符串代表一幅PNG格式的玫瑰图片:iVBORw0KGg…Jggg==。为了确保这不是一些随机字符,将其复制粘贴(没有双引号)到任何一个便利的“Base64图像”解码器,例如 codebeautify.org/base64-to-image-converter 。你应该可以看到一幅美丽的玫瑰图片:
示例图像(红玫瑰)
现在,让我们显示它周围的nodejs包装器的剩余部分,完成图像处理服务。我们将会提供一些不同的操作:
•Ping:验证服务可用。
•getDimensions:调用identify操作来提取图像宽度和高度的简写。
•identify:提取图像元数据。
•Resize:重新定义大小(在内部调用convert程序)的便利程序。
•Thumbnail:resize的同义词。
•convert:万能程序–可以转换媒体格式,运用转换,重新定义大小等。
•getSample:提取一个示例图像;是“hello world”操作。
代码的大部分是对nodejs ImageMagick程序非常简单的包装,这些程序中的一些采用JSON格式(在这种情况下,被传递到Lambda的事件被清扫干净并被转发出去),另一些程序则采用命令行(也称作,自定义)参数,这些参数作为一个字符串组被传递。如果你以前没有使用过ImageMagick,这其中可能不明显的一部分是它作为一种包装器,运行于命令行之上,文件名称有语义含义。我们有两个相互矛盾的需求:我们想要客户端传达语义学含义(例如,图像的输出格式,如PNG格式还是JPEG格式),但是我们想要服务的创建者来决定磁盘上的临时存储位置以便我们不会泄露实施细节。为了一次完成这两个需求,我们在JSON模式中定义了两个参数:“inputExtension”和“outputExtension”,然后我们将客户端的一部分(文件扩展名)和服务器的一部分(目录和基名)结合来构建文件的实际位置。你可以在 图像处理蓝图 中看到(并使用)已完成的代码。
这里你可以运行很多测试(后面我们会做更多),但是作为一个快速的健康检查,再次提取示例玫瑰图像,然后使用一个否定(颜色反转)过滤器将其传递回去。你可以在Lambda控制台使用像下面一样的JSON命令,只需将base64Image字段替换成实际的图像字符(将这里写在博客页上有点长)。
{ "operation": "convert", "customArgs": [ "-negate" ], "outputExtension": "png", "base64Image": "...fill this in with the rose sample image, base64-encoded..." }
输出结果,被解码成一幅图像,应该是不可捉摸的植物,一支蓝色玫瑰:
蓝色玫瑰(反转红色玫瑰示例图像的结果)
所以,剩下的就是该服务的功能方面。一般地,这就是它开始变得令人厌恶的地方,从“一次性运作”到“可伸缩,可靠的服务,提供全年每天24小时的监控和生产日志”。但是,那正是Lambda的伟大之处:我们的图像处理代码已经是一个全面部署的,具有生产优势的微服务。下面,让我们添加一个可以调用该服务的移动应用...
第二步:创建一个移动客户端
你可以通过许多方式访问我们的图像处理微服务,但是为了展示一个示例客户端,我们将会构建一个简易Android应用。下面我将展示在ContainerCon演讲中用来创建简易Android应用的客户端侧代码,该简易Android应用可以使你选取一个图像,一个过滤器,然后通过AWS Lambda中正在运行的图像处理服务中调用convert操作来展示应用过滤器到图像所产生的效果。
为了直观地了解应用所做的事情,下面是它的一个示例图像,AWS Lambda Icon:
Android模拟器展示AWS Lambda Icon图像
我们将会挑选出“negate”过滤器来转换图标中的颜色
挑选‘Negate’图像转换过滤器
..and here’s the result: A blue version of our (originally orange) Lambda moniker:下面是转换的结果:我们的(最初是橙色的)Lambda标记的蓝色版本:
挑选‘Negate’图像转换过滤器
..and here’s the result: A blue version of our (originally orange) Lambda moniker:下面是转换的结果:我们的(最初是橙色的)Lambda标记的蓝色版本:
‘Negate’过滤器应用于AWS Lambda图标的结果
我们也可以选择西雅图图像,应用一个陈旧效果过滤器来给现代的西雅图天际线一种古老世界的感觉:
富有陈旧效果的西雅图天际线
现在让我们来讨论代码。在这里我不是在努力地教大家基本的Android编程,所以我将只聚焦于该应用中Lambda特定的元素。(如果你正在创建自己的应用,你也会需要包含 AWS Mobile SDK jar 文件来运行下面的示例代码。)概念上讲,有以下四部分:
1.POJO数据模式
2.远程服务(操作)定义
3.初始化
4.服务调用
我们将会依次看一下每一部分:
数据模式定义了需要在客户端和服务器之间传递的对象。在这里没有Lambda机制;这些对象只是传统Java对象(Plain Old Java Objects,POJOs ),没有特别的库或框架。我们定义一个基本事件,然后将其扩展来反映我们的操作结构–你可以把这想象成JSON格式的Java化,在上面定义和测试图像处理服务时我们曾用到过JSON格式。如果你当时也是以Java格式写入服务器,很典型地,你将将这些文件作为常见的事件结构定义过程的一部分进行共享;在我们的示例中,这些POJO在服务器侧转化成JSON格式。
LambdaEvent.java
package com.amazon.lambda.androidimageprocessor.lambda; public class LambdaEvent { private String operation; public String getOperation() {return operation;} public void setOperation(String operation) {this.operation = operation;} public LambdaEvent(String operation) {setOperation(operation);} }ImageConvertRequest.java
package com.amazon.lambda.androidimageprocessor.lambda; import java.util.List; public class ImageConvertRequest extends LambdaEvent { private String base64Image; private String inputExtension; private String outputExtension; private List customArgs; public ImageConvertRequest() {super("convert");} public String getBase64Image() {return base64Image;} public void setBase64Image(String base64Image) {this.base64Image = base64Image;} public String getInputExtension() {return inputExtension;} public void setInputExtension(String inputExtension) {this.inputExtension = inputExtension;} public String getOutputExtension() {return outputExtension;} public void setOutputExtension(String outputExtension) {this.outputExtension = outputExtension;} public List getCustomArgs() {return customArgs;} public void setCustomArgs(List customArgs) {this.customArgs = customArgs;} }
到目前为止,情况还不是很复杂。既然我们有了一个数据模型,我们将使用一些Java注解来定义服务的端点。在这里我们将会揭露两个操作,“ping”和“convert”;将其进行扩展来包含其他的操作会比较简单,但是下面的示例应用,我们不需要其他的操作。
ILambdaInvoker.java
package com.amazon.lambda.androidimageprocessor.lambda; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunction; import java.util.Map; public interface ILambdaInvoker { @LambdaFunction(functionName = "ImageProcessor") String ping(Map event); @LambdaFunction(functionName = "ImageProcessor") String convert(ImageConvertRequest request); }
现在我们已经准备好做应用的主要部分了。这其中的多数是Android样板代码或简单的客户端侧资源管理,但是我们将指出Lambda相关的两个部分:
这就是“init”部分,它创建鉴权供应商来调用Lambda API,创建一个Lambda调用器,该调用器可以调用上面定义的端点,在我们的数据模型中传递POJO:
// Create an instance of CognitoCachingCredentialsProvider CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider( this.getApplicationContext(), "us-east-1:<YOUR COGNITO IDENITY POOL GOES HERE>", Regions.US_EAST_1); // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy. LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(), Regions.US_EAST_1, cognitoProvider); // Create the Lambda proxy object with a default Json data binder. lambda = factory.build(ILambdaInvoker.class);
另一个有趣(嗯,是有点有趣)的代码部分是实际的远程步骤调用本身:
try { return lambda.convert(params[0]); } catch (LambdaFunctionException e) { Log.e("Tag", "Failed to convert image"); return null; }
实际上它并不是那么有趣因为魔法(参数序列化和结果还原序列化)正在后台发生,这里只留下一些错误需要处理。
下面是完整的源文件:
package com.amazon.lambda.androidimageprocessor; import android.app.Activity; import android.app.ProgressDialog; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.util.Base64; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.Spinner; import android.widget.Toast; import com.amazon.lambda.androidimageprocessor.lambda.ILambdaInvoker; import com.amazon.lambda.androidimageprocessor.lambda.ImageConvertRequest; import com.amazonaws.auth.CognitoCachingCredentialsProvider; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunctionException; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaInvokerFactory; import com.amazonaws.regions.Regions; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class MainActivity extends Activity { private ILambdaInvoker lambda; private ImageView selectedImage; private String selectedImageBase64; private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Create an instance of CognitoCachingCredentialsProvider CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider( this.getApplicationContext(), "us-east-1:2a40105a-b330-43cf-8d4e-b647d492e76e", Regions.US_EAST_1); // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy. LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(), Regions.US_EAST_1, cognitoProvider); // Create the Lambda proxy object with a default Json data binder. lambda = factory.build(ILambdaInvoker.class); // ping lambda function to make sure everything is working pingLambda(); } // ping the lambda function @SuppressWarnings("unchecked") private void pingLambda() { Map event = new HashMap(); event.put("operation", "ping"); // The Lambda function invocation results in a network call. // Make sure it is not called from the main thread. new AsyncTask<Map, Void, String>() { @Override protected String doInBackground(Map... params) { // invoke "ping" method. In case it fails, it will throw a // LambdaFunctionException. try { return lambda.ping(params[0]); } catch (LambdaFunctionException lfe) { Log.e("Tag", "Failed to invoke ping", lfe); return null; } } @Override protected void onPostExecute(String result) { if (result == null) { return; } // Display a quick message Toast.makeText(MainActivity.this, "Made contact with AWS lambda", Toast.LENGTH_LONG).show(); } }.execute(event); } // event handler for "process image" button public void processImage(View view) { // no image has been selected yet if (selectedImageBase64 == null) { Toast.makeText(this, "Please tap one of the images above", Toast.LENGTH_LONG).show(); return; } // get selected filter String filter = ((Spinner) findViewById(R.id.filter_picker)).getSelectedItem().toString(); // assemble new request ImageConvertRequest request = new ImageConvertRequest(); request.setBase64Image(selectedImageBase64); request.setInputExtension("png"); request.setOutputExtension("png"); // custom arguments per filter List customArgs = new ArrayList(); request.setCustomArgs(customArgs); switch (filter) { case "Sepia": customArgs.add("-sepia-tone"); customArgs.add("65%"); break; case "Black/White": customArgs.add("-colorspace"); customArgs.add("Gray"); break; case "Negate": customArgs.add("-negate"); break; case "Darken": customArgs.add("-fill"); customArgs.add("black"); customArgs.add("-colorize"); customArgs.add("50%"); break; case "Lighten": customArgs.add("-fill"); customArgs.add("white"); customArgs.add("-colorize"); customArgs.add("50%"); break; default: return; } // async request to lambda function new AsyncTask() { @Override protected String doInBackground(ImageConvertRequest... params) { try { return lambda.convert(params[0]); } catch (LambdaFunctionException e) { Log.e("Tag", "Failed to convert image"); return null; } } @Override protected void onPostExecute(String result) { // if no data was returned, there was a failure if (result == null || Objects.equals(result, "")) { hideLoadingDialog(); Toast.makeText(MainActivity.this, "Processing failed", Toast.LENGTH_LONG).show(); return; } // otherwise decode the base64 data and put it in the selected image view byte[] imageData = Base64.decode(result, Base64.DEFAULT); selectedImage.setImageBitmap(BitmapFactory.decodeByteArray(imageData, 0, imageData.length)); hideLoadingDialog(); } }.execute(request); showLoadingDialog(); } /* Select methods for each image */ public void selectLambdaImage(View view) { selectImage(R.drawable.lambda); selectedImage = (ImageView) findViewById(R.id.static_lambda); Toast.makeText(this, "Selected image 'lambda'", Toast.LENGTH_LONG).show(); } public void selectSeattleImage(View view) { selectImage(R.drawable.seattle); selectedImage = (ImageView) findViewById(R.id.static_seattle); Toast.makeText(this, "Selected image 'seattle'", Toast.LENGTH_LONG).show(); } public void selectSquirrelImage(View view) { selectImage(R.drawable.squirrel); selectedImage = (ImageView) findViewById(R.id.static_squirrel); Toast.makeText(this, "Selected image 'squirrel'", Toast.LENGTH_LONG).show(); } public void selectLinuxImage(View view) { selectImage(R.drawable.linux); selectedImage = (ImageView) findViewById(R.id.static_linux); Toast.makeText(this, "Selected image 'linux'", Toast.LENGTH_LONG).show(); } // extract the base64 encoded data of the drawable resource `id` private void selectImage(int id) { Bitmap bmp = BitmapFactory.decodeResource(getResources(), id); ByteArrayOutputStream stream = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.PNG, 100, stream); selectedImageBase64 = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT); } // reset images to their original state public void reset(View view) { ((ImageView) findViewById(R.id.static_lambda)).setImageDrawable(getResources().getDrawable(R.drawable.lambda, getTheme())); ((ImageView) findViewById(R.id.static_seattle)).setImageDrawable(getResources().getDrawable(R.drawable.seattle, getTheme())); ((ImageView) findViewById(R.id.static_squirrel)).setImageDrawable(getResources().getDrawable(R.drawable.squirrel, getTheme())); ((ImageView) findViewById(R.id.static_linux)).setImageDrawable(getResources().getDrawable(R.drawable.linux, getTheme())); Toast.makeText(this, "Please choose from one of these images", Toast.LENGTH_LONG).show(); } private void showLoadingDialog() { progressDialog = ProgressDialog.show(this, "Please wait...", "Processing image", true, false); } private void hideLoadingDialog() { progressDialog.dismiss(); } }
移动应用上就是这些:一个数据模型(也称作Java类别),一个控制模型(也称作两个方法),三个初始化语句,然后是携带一个try/catch代码块的远程调用...很简单的东西。
多区域部署
到目前为止,我们还没怎么说及代码的运行位置。Lambda负责在一个区域内部署代码,但是你需要决定你想要在哪些区域运行该代码。在我最初的演示中,我最初在us-east-1区域,也称作Virginia数据中心,构建了该函数。为了兑现我们在摘要中所做的构建全球服务的宣言,让我们将区域扩展至eu-west-1 (Ireland)和ap-northeast-1 (Tokyo)以便移动应用可以从世界各地进行低时延的连接:
无服务器机制,在两个其他区域部署Lambda函数
这一个我们已经在博客中讨论过了:在 S3部署帖 中,我展示了如何使用一个Lambda函数在Amazon S3中部署以ZIP文件形式存储的其他多个Lambda函数。在ContainerCon的演讲中,我们也打开了 S3跨区域 复制功能,使其变得稍微时髦了一些,这样我们就可以以ZIP文件的形式上传图像处理服务到爱尔兰,使S3自动将服务复制到东京,然后使两个区域在各自的区域中将服务自动部署到相关联的Lambda服务中。你会爱上无服务器解决方案的)。
无服务器Web应用,第一部分:API端点
既然我们已经拥有了一个移动应用和一个全球部署的图像处理服务作为移动应用的后端,让我们将注意力转移到为那些偏爱浏览器胜过设备的人创建一个无服务器web应用。我们将会分两部分完成:首先,我们会为图像处理服务创建一个API端点。然后在下一部分,我们将会使用Amazon S3添加实际的网站。
AWS Lambda使代码转换为服务变得简单的方法之一就是提供一个web服务前端,“built in”。但是,这需要客户端(像我们在上一部分构建的移动客户端)使用AWS提供的证书来签署请求。这在我们的Android应用中是由 Amazon Cognito 鉴权客户端处理的,但是如果我们想通过一个网站提供图像处理服务的公共访问又会怎样呢?
为了达到这一目的,我们将会求助于另一个服务器, Amazon API Gateway 。这项服务使你能够定义一个API而不需要任何基础设施–API完全受AWS管理。我们将会使用API网关来为图像处理服务创建一个URL,该URL为网上的任意一个用户提供对它的一个能力子集的访问。Amazon API Gateway提供各种方法来控制对API的访问:API调用可以使用AWS证书来签署,你可以使用OAuth令牌,仅转发令牌头域进行验证,你可以使用API密钥(不推荐作为确保访问安全的方法),或将一个API完全公开,正如我们这里将会展示的。
除了各种各样的访问模型,API Gateway还有许多特性,在这篇帖子中我们就不进行探索了。一些特性是内置的(像反DDOS保护),其他的,像缓存,会使我们在反复提取流行图像时能够进一步缩短时延,降低成本。通过在客户端和(微)服务之间插入一个间接层,API Gateway也使通过它的版本和阶段特性独立地演进各个服务变成了可能。但是现在,我们将会聚焦于基本任务,将图像处理服务作为一个API进行显示。
好,让我们创建我们的API。在AWS Console中,选择API Gateway,然后选择“New API”,为该API指定一个名称并提供一个选择性描述。在我的示例中,我将其命名为“ImageAPI”。
下一步,为你的新API创建一项资源(我称其为“ImageProcessingService”),然后在资源内部创建一个POST方法。选择“Lambda function”作为集成类型,输入你正在作为图像处理服务使用的Lambda函数的名称。在“Method Request”配置区域中,将授权类型设置为“none”(也就是说,这将会是一个可以公开访问的端点)。这就差不多了。
为了测试集成结果,点击“Test”按钮:
然后提供一个测试负载,如{“operation”: “ping”}。你应该能够得到预期的“pong”结果,表明你已经成功地将你的API连接到了Lambda函数。
除此之外,后面我们将会有更多(更深层次)的测试,但是我发现有时很有用的一件事是在API中,在顶层资源中添加一个GET方法,一定会使一些事情变得简单,像ping操作,使我也能够按照预期,快速地从我的API连接的任何一个浏览器诊断我的Lambda函数。这次(或一般)演示不需要,但是你也可能发现它是有用的。
为了接下来的内容(S3静态内容),我们也需要打开CORS功能。这很简单但是有好几个步骤。API Gateway小组在继续努力,想使这变得更简单,所以没有在这里重复那些指令(可能很快就会使它们变得过时),我推荐您查看文档。
点击“Deploy this API”按钮。这样你应该就准备好创建网站了!
无服务器web应用,第二部分:Amazon S3承载的静态网站
这一部分很简单–上传下面的Javascript网站代码到你选择的S3桶中:
var ENDPOINT = 'https://fuexvelc41.execute-api.us-east-1.amazonaws.com/prod/ImageProcessingService'; angular.module('app', ['ui.bootstrap']) .controller('MainController', ['$scope', '$http', function($scope, $http) { $scope.loading = false; $scope.image = { width: 100 }; $scope.ready = function() { $scope.loading = false; }; $scope.submit = function() { var fileCtrl = document.getElementById('image-file'); if (fileCtrl.files && fileCtrl.files[0]) { $scope.loading = true; var fr = new FileReader(); fr.onload = function(e) { $scope.image.base64Image = e.target.result.slice(e.target.result.indexOf(',') + 1); $scope.$apply(); document.getElementById('original-image').src = e.target.result; // Now resize! $http.post(ENDPOINT, angular.extend($scope.image, { operation: 'resize', outputExtension: fileCtrl.value.split('.').pop() })) .then(function(response) { document.getElementById('processed-image').src = "data:image/png;base64," + response.data; }) .catch(console.log) .finally($scope.ready); }; fr.readAsDataURL(fileCtrl.files[0]); } }; }]);
下面是我们在演示中用于(很基本的)网站的HTML源:
<!DOCTYPE html> <html lang="en"> <head> <title>Image Processing Service</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Open+Sans:400,700"> <link rel="stylesheet" type="text/css" href="main.css"> </head> <body ng-app="app" ng-controller="MainController"> <div class="container"> <h1>Image Processing Service</h1> <div class="row"> <div class="col-md-4"> <form ng-submit="submit()"> <div class="form-group"> <label for="image-file">Image</label> <input id="image-file" type="file"> </div> <div class="form-group"> <label for="image-width">Width</label> <input id="image-width" class="form-control" type="number" ng-model="image.width" min="1" max="4096"> </div> <button type="submit" class="btn btn-primary"> <span class="glyphicon glyphicon-refresh" ng-if="loading"></span> Submit </button> </form> </div> <div class="col-md-8"> <accordion close-others="false"> <accordion-group heading="Original Image" is-open="true"> <img id="original-image" class="img-responsive"> </accordion-group> <accordion-group heading="Processed Image" is-open="true"> <img id="processed-image" class="img-responsive"> </accordion-group> </accordion> </div> </div> </div> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap-tpls.min.js"></script> <script type="text/javascript" src="main.js"></script> </body> </html>
最后,这是CSS:
body { font-family: 'Open Sans', sans-serif; padding-bottom: 15px; } a { cursor: pointer; } /** LOADER **/ .glyphicon-refresh { -animation: spin .7s infinite linear; -webkit-animation: spin .7s infinite linear; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @-webkit-keyframes spin { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } }
URL将取决于你的S3区域和对象的名称,如“http://image-processing-service.s3-website-us-east-1.amazonaws.com/”。在一个浏览器中访问该URL,你应该能够看到你的图像网站:
单元和负载测试
有API Gateway为你的Lambda微服务提供一个传统的基于URL的接口,你有许多种选择进行测试。但是让我们坚持我们的无服务器方法,完全在没有基础设施或甚至客户端的情况下进行测试!
首先,我们想通过API进行调用。那很简单;我们使用Lambda的HTTPS调用蓝图来传到我们使用API Gateway部署时拥有的端点:
{ "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" } }
现在我们拥有了样本,让我们在它周围包装一个单元测试。我们的单元测试工具并不是很有效果;它只是运行另一个Lambda函数,将结果存储到我们指定的一个Amazon DynamoDB表中。在“单元测试”模式下,我们将会使用 单元和负载测试工具Lambda蓝图 :
{ "operation": "unit", "function": "HTTPSInvoker", "resultsTable": "unit-test-results", "testId": "LinuxConDemo", "event": { "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" } } }
最后,我们将会多次运行单元测试来做一个简单的负载测试。我们将会再次使用Lambda 单元负载测试工具 ,这次是在“负载测试”模式下:
{ "operation": "load", "iterations": 100, "function": "TestHarness", "event": { "operation": "unit", "function": "HTTPSInvoker", "resultsTable": "unit-test-results", "testId": "LinuxConLoadTestDemo", "event": { "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" } } } }
下面是我们的无服务器测试架构的一张图片:
一个无服务器单元和负载测试工具
你可以轻松地变换这种方法,并入校验程序,运行各种单元测试等。如果你不需要web应用基础设施,你可以跳过API Gateway和HTTP调用,直接在你的单元测试中只运行图像处理服务。如果你想总结或分析测试输出,你可以轻松地将一个Lambda函数作为一个事件处理器附加到存储测试结果的DynamoDB表中。
总结
这是一篇稍长的帖子,但是它是一个完整的程序包,包含构建一个真实的,可伸缩的后端服务,使其与移动客户端和一个网站相对,所有这些在系统的任何一部分,前端,后端,API,部署或测试中,都不需服务器或其他基础设。让我们走向无服务器时代!
原文链接: https://aws.amazon.com/cn/blogs/compute/microservices-without-the-servers/活动推荐: 8月18日亚马逊AWS云计算研讨会之云中的安全部署与开发运维(厦门站)
( 翻译/吕冬梅 责编/王鑫贺 )
订阅“AWS中文技术社区”微信公众号,实时掌握AWS技术及产品消息!
AWS中文技术社区为广大开发者提供了一个Amazon Web Service技术交流平台 ,推送AWS最新资讯、技术视频、技术文档、精彩技术博文等相关精彩内容,更有AWS社区专家与您直接沟通交流!快加入AWS中文技术社区,更快更好的了解AWS云计算技术。